Merge branch 'coollabsio:main' into add-debugbar

This commit is contained in:
peaklabs-dev
2024-09-18 20:42:29 +02:00
committed by GitHub
73 changed files with 2051 additions and 646 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -21,6 +21,7 @@ jobs:
packages: write
attestations: write
id-token: write
actions: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
@@ -44,6 +45,7 @@ jobs:
packages: write
attestations: write
id-token: write
actions: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
@@ -67,6 +69,7 @@ jobs:
packages: write
attestations: write
id-token: write
actions: write
needs: [amd64, aarch64]
steps:
- name: Checkout

View File

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

View File

@@ -1,17 +1,27 @@
# Contributing
# Contributing to Coolify
> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
## Table of Contents
## Code Contribution
1. [Setup Development Environment](#1-setup-development-environment)
2. [Verify Installation](#2-verify-installation-optional)
3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
4. [Set up Environment Variables](#4-set-up-environment-variables)
5. [Start Coolify](#5-start-coolify)
6. [Start Development](#6-start-development)
7. [Development Notes](#7-development-notes)
8. [Create a Pull Request](#8-create-a-pull-request)
9. [Additional Contribution Guidelines](#additional-contribution-guidelines)
## 1. Setup your development environment
## 1. Setup Development Environment
Follow the steps below for your operating system:
### Windows
<details>
<summary><strong>Windows</strong></summary>
1. Install `docker-ce`, Docker Desktop (or similar):
- Docker CE (recommended):
@@ -25,7 +35,10 @@ Follow the steps below for your operating system:
2. Install Spin:
- Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2)
### MacOS
</details>
<details>
<summary><strong>MacOS</strong></summary>
1. Install Orbstack, Docker Desktop (or similar):
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
@@ -36,7 +49,10 @@ Follow the steps below for your operating system:
2. Install Spin:
- Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin)
### Linux
</details>
<details>
<summary><strong>Linux</strong></summary>
1. Install Docker Engine, Docker Desktop (or similar):
- Docker Engine (recommended, as there is no VM overhead):
@@ -47,8 +63,9 @@ Follow the steps below for your operating system:
2. Install Spin:
- Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions)
</details>
## 2. Verify installation (optional)
## 2. Verify Installation (Optional)
After installing Docker (or Orbstack) and Spin, verify the installation:
@@ -60,25 +77,20 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
```
You should see version information for both Docker and Spin.
## 3. Fork the Coolify repository and setup your local repository
## 3. Fork and Setup Local Repository
1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account.
2. Install a code editor on your machine (below are some popular choices, choose one):
2. Install a code editor on your machine (choose one):
- Visual Studio Code (recommended free):
- Windows/macOS/Linux: Download and install from [https://code.visualstudio.com/download](https://code.visualstudio.com/download)
- Cursor (recommended but paid for getting the full benefits):
- Windows/macOS/Linux: Download and install from [https://www.cursor.com/](https://www.cursor.com/)
- Zed (very fast code editor):
- macOS/Linux: Download and install from [https://zed.dev/download](https://zed.dev/download)
- Windows: Not available yet
| Editor | Platform | Download Link |
|--------|----------|---------------|
| Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) |
| Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) |
| Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) |
3. Clone the Coolify Repository from your fork to your local machine
- Use `git clone` in the command line
- Use `git clone` in the command line, or
- Use GitHub Desktop (recommended):
- Download and install from [https://desktop.github.com/](https://desktop.github.com/)
- Open GitHub Desktop and login with your GitHub account
@@ -86,37 +98,32 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
4. Open the cloned Coolify Repository in your chosen code editor.
## 4. Set up Environment Variables
1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
5. Save the changes to your `.env` file.
## 5. Start Coolify
1. Open a terminal in the local Coolify directory.
2. Run the following command in the terminal (leave that terminal open):
```
```bash
spin up
```
Note: You may see some errors, but don't worry; this is expected.
> [!NOTE]
> You may see some errors, but don't worry; this is expected.
3. If you encounter permission errors, especially on macOS, use:
```
```bash
sudo spin up
```
Note: If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
> [!NOTE]
> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
## 6. Start Development
@@ -126,15 +133,17 @@ Note: If you change environment variables afterwards or anything seems broken, p
- Password: `password`
2. Additional development tools:
- Laravel Horizon (scheduler): `http://localhost:8000/horizon`
Note: Only accessible when logged in as root user
- Mailpit (email catcher): `http://localhost:8025`
- Telescope (debugging tool): `http://localhost:8000/telescope`
Note: Disabled by default (so the database is not overloaded), enable by adding the following environment variable to your `.env` file:
```env
TELESCOPE_ENABLED=true
```
| Tool | URL | Note |
|------|-----|------|
| Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
| Mailpit (email catcher) | `http://localhost:8025` | |
| Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default |
> [!NOTE]
> To enable Telescope, add the following to your `.env` file:
> ```env
> TELESCOPE_ENABLED=true
> ```
## 7. Development Notes
@@ -150,18 +159,12 @@ When working on Coolify, keep the following in mind:
docker exec -it coolify php artisan migrate:fresh --seed
```
3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any envrionement specific issues.
3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues.
Remember, forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
> [!IMPORTANT]
> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
## 8. Contributing a New Service
To add a new service to Coolify, please refer to our documentation:
[Adding a New Service](https://coolify.io/docs/knowledge-base/add-a-service)
## 9. Create a Pull Request
## 8. Create a Pull Request
1. After making changes or adding a new service:
- Commit your changes to your forked repository.
@@ -179,11 +182,26 @@ To add a new service to Coolify, please refer to our documentation:
- In the description, explain the changes you've made.
- Reference any related issues by using keywords like "Fixes #123" or "Closes #456".
4. Important note:
Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
> [!IMPORTANT]
> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
5. Submit your PR:
4. Submit your PR:
- Review your changes one last time.
- Click "Create pull request" to submit.
> [!NOTE]
> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
After submission, maintainers will review your PR and may request changes or provide feedback.
## Additional Contribution Guidelines
### Contributing a New Service
To add a new service to Coolify, please refer to our documentation:
[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service)
### Contributing to Documentation
To contribute to the Coolify documentation, please refer to this guide:
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md)

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -55,6 +56,13 @@ class UpdateCoolify
return;
}
$all_servers = Server::all();
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
PullHelperImageJob::dispatch($server);
}
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
remote_process([

View File

@@ -31,7 +31,7 @@ class StartService
$network = $service->destination->network;
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');

View File

@@ -514,7 +514,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'hidden' => true,
'ignore_errors' => true,
], [
"docker network connect {$networkId} coolify-proxy || true",
"docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
'hidden' => 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->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()) {
$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->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()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
}
}
@@ -2049,6 +2049,10 @@ 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"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2068,6 +2072,10 @@ 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"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2110,6 +2118,10 @@ 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"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2129,6 +2141,10 @@ 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"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2157,6 +2173,10 @@ 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"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2176,6 +2196,10 @@ 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"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,76 @@
<?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,6 +305,13 @@ respond 404
'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
],
'coolify-terminal-ws' => [
'entryPoints' => [
0 => 'http',
],
'service' => 'coolify-terminal',
'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)",
],
],
'services' => [
'coolify' => [
@@ -325,6 +332,15 @@ respond 404
],
],
],
'coolify-terminal' => [
'loadBalancer' => [
'servers' => [
0 => [
'url' => 'http://coolify-realtime:6002',
],
],
],
],
],
],
];
@@ -354,6 +370,16 @@ respond 404
'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 =
@@ -387,6 +413,9 @@ $schema://$host {
handle /app/* {
reverse_proxy coolify-realtime:6001
}
handle /terminal/ws {
reverse_proxy coolify-realtime:6002
}
reverse_proxy coolify:80
}";
$base64 = base64_encode($caddy_file);
@@ -746,6 +775,18 @@ $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
{
if ($this->isFunctional()) {

View File

@@ -32,6 +32,16 @@ class ServiceApplication extends BaseModel
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()
{
return data_get($this, 'is_log_drain_enabled', false);

View File

@@ -25,6 +25,16 @@ class ServiceDatabase extends BaseModel
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()
{
return data_get($this, 'is_log_drain_enabled', false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,6 +40,20 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
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
{
$outputLines = explode(PHP_EOL, $rawOutput);
@@ -215,12 +229,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([
'value' => generateFqdn($server, 'console-'.$uuid),
'value' => generateFqdn($server, 'console-'.$uuid, true),
]);
}
if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([
'value' => generateFqdn($server, 'minio-'.$uuid),
'value' => generateFqdn($server, 'minio-'.$uuid, true),
]);
}
$payload = collect([

View File

@@ -478,7 +478,7 @@ function data_get_str($data, $key, $default = null): Stringable
return str($str);
}
function generateFqdn(Server $server, string $random): string
function generateFqdn(Server $server, string $random, bool $forceHttps = false): string
{
$wildcard = data_get($server, 'settings.wildcard_domain');
if (is_null($wildcard) || $wildcard === '') {
@@ -488,6 +488,9 @@ function generateFqdn(Server $server, string $random): string
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
if ($forceHttps) {
$scheme = 'https';
}
$finalFqdn = "$scheme://{$random}.$host$path";
return $finalFqdn;
@@ -786,7 +789,7 @@ function replaceLocalSource(Stringable $source, Stringable $replacedWith)
if ($source->startsWith('..')) {
$source = $source->replaceFirst('..', $replacedWith->value());
}
if ($source->endsWith('/')) {
if ($source->endsWith('/') && $source->value() !== '/') {
$source = $source->replaceLast('/', '');
}
@@ -2100,16 +2103,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// TODO: move this in a shared function
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')) {
$parsedServiceVariables->put('COOLIFY_SERVER_IP', $resource->destination->server->ip);
$parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\"");
}
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')) {
$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) {
@@ -2982,7 +2985,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
// Get magic environments where we need to preset the FQDN
if ($key->startsWith('SERVICE_FQDN_')) {
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
if (substr_count(str($key)->value(), '_') === 3) {
$fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
} else {
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
}
if ($isApplication) {
$fqdn = generateFqdn($server, "{$resource->name}-$uuid");
} elseif ($isService) {
@@ -3229,7 +3236,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($isApplication && $isPullRequest) {
$source = $source."-pr-$pullRequestId";
}
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
@@ -3469,13 +3475,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$branch = "pull/{$pullRequestId}/head";
}
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
if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', $containerName);
$coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "\"{$containerName}\"");
}
if ($isApplication) {
@@ -3548,7 +3554,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($isApplication) {
$shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
$uuid = $resource->uuid;
$network = $resource->destination->network;
$network = data_get($resource, 'destination.network');
if ($isPullRequest) {
$uuid = "{$resource->uuid}-{$pullRequestId}";
}
@@ -3558,7 +3564,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} else {
$shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels;
$uuid = $resource->uuid;
$network = $resource->destination->network;
$network = data_get($resource, 'destination.network');
}
if ($shouldGenerateLabelsExactly) {
switch ($server->proxyType()) {
@@ -3723,30 +3729,30 @@ function add_coolify_default_environment_variables(StandaloneRedis|StandalonePos
}
if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_APP_NAME')->isEmpty()) {
if ($isAssociativeArray) {
$where_to_add->put('COOLIFY_APP_NAME', $resource->name);
$where_to_add->put('COOLIFY_APP_NAME', "\"{$resource->name}\"");
} 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 ($isAssociativeArray) {
$where_to_add->put('COOLIFY_SERVER_IP', $ip);
$where_to_add->put('COOLIFY_SERVER_IP', "\"{$ip}\"");
} 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 ($isAssociativeArray) {
$where_to_add->put('COOLIFY_ENVIRONMENT_NAME', $resource->environment->name);
$where_to_add->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\"");
} 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 ($isAssociativeArray) {
$where_to_add->put('COOLIFY_PROJECT_NAME', $resource->project()->name);
$where_to_add->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\"");
} 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
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.335',
'release' => '4.0.0-beta.341',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.335';
return '4.0.0-beta.341';

View File

@@ -44,10 +44,17 @@ services:
- /data/coolify/_volumes/redis/:/data
# - coolify-redis-data-dev:/data
soketi:
build:
context: .
dockerfile: ./docker/coolify-realtime/Dockerfile
env_file:
- .env
ports:
- "${FORWARD_SOKETI_PORT:-6001}:6001"
- "6002:6002"
volumes:
- ./storage:/var/www/html/storage
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
environment:
SOKETI_DEBUG: "false"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,13 @@
{
"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

@@ -0,0 +1,27 @@
#!/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

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

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

View File

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

View File

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

View File

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

View File

@@ -5,11 +5,30 @@ set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check
#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
CDN="https://cdn.coollabs.io/coolify-nightly"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.4"
VERSION="1.5"
DOCKER_VERSION="26.0"
CDN="https://cdn.coollabs.io/coolify-nightly"
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
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 '"')
ENV_FILE="/data/coolify/source/.env"
@@ -46,12 +65,16 @@ fi
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_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
if [ -z "$LATEST_HELPER_VERSION" ]; then
LATEST_HELPER_VERSION=latest
fi
DATE=$(date +"%Y%m%d-%H%M%S")
if [ -z "$LATEST_REALTIME_VERSION" ]; then
LATEST_REALTIME_VERSION=latest
fi
if [ $EUID != 0 ]; then
echo "Please run as root"
@@ -73,18 +96,29 @@ if [ "$1" != "" ]; then
LATEST_VERSION="${LATEST_VERSION#v}"
fi
echo -e "-------------"
echo -e "Welcome to Coolify v4 beta installer!"
echo -e "This script will install everything for you."
echo -e "\033[0;35m"
cat << "EOF"
_____ _ _ __
/ ____| | (_)/ _|
| | ___ ___ | |_| |_ _ _
| | / _ \ / _ \| | | _| | | |
| |___| (_) | (_) | | | | | |_| |
\_____\___/ \___/|_|_|_| \__, |
__/ |
|___/
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 "-------------"
echo "OS: $OS_TYPE $OS_VERSION"
echo "Coolify version: $LATEST_VERSION"
echo "Helper version: $LATEST_HELPER_VERSION"
echo -e "-------------"
echo "Installing required packages..."
echo -e "---------------------------------------------"
echo "| Operating System | $OS_TYPE $OS_VERSION"
echo "| Docker | $DOCKER_VERSION"
echo "| Coolify | $LATEST_VERSION"
echo "| Helper | $LATEST_HELPER_VERSION"
echo "| Realtime | $LATEST_REALTIME_VERSION"
echo -e "---------------------------------------------\n"
echo -e "1. Installing required packages (curl, wget, git, jq). "
case "$OS_TYPE" in
arch)
@@ -122,24 +156,26 @@ sles | opensuse-leap | opensuse-tumbleweed)
;;
esac
echo -e "2. Check OpenSSH server configuration. "
# Detect OpenSSH server
SSH_DETECTED=false
if [ -x "$(command -v systemctl)" ]; then
if systemctl status sshd >/dev/null 2>&1; then
echo "OpenSSH server is installed."
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
if systemctl status ssh >/dev/null 2>&1; then
echo "OpenSSH server is installed."
elif systemctl status ssh >/dev/null 2>&1; then
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
elif [ -x "$(command -v service)" ]; then
if service sshd status >/dev/null 2>&1; then
echo "OpenSSH server is installed."
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
if service ssh status >/dev/null 2>&1; then
echo "OpenSSH server is installed."
elif service ssh status >/dev/null 2>&1; then
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
fi
@@ -151,104 +187,91 @@ if [ "$SSH_DETECTED" = "false" ]; then
fi
# Detect SSH PermitRootLogin
SSH_PERMIT_ROOT_LOGIN=false
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)"
if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "without-password" ]; then
echo "PermitRootLogin is enabled."
SSH_PERMIT_ROOT_LOGIN=true
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 "###############################################################################"
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then
echo " - SSH PermitRootLogin is enabled."
else
echo " - SSH PermitRootLogin is disabled."
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh"
fi
# Detect if docker is installed via snap
if [ -x "$(command -v snap)" ]; then
if snap list | grep -q docker; then
echo "Docker is installed via snap."
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
echo " - Docker is 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."
exit 1
fi
fi
echo -e "3. Check Docker Installation. "
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker is not installed. Installing Docker. It may take a while."
getAJoke
case "$OS_TYPE" in
"almalinux")
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
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
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
fi
systemctl start docker
systemctl enable docker
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
;;
"alpine")
apk add docker docker-cli-compose
rc-update add docker default
service docker start
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Failed to install Docker with apk. Try to install it manually."
apk add docker docker-cli-compose >/dev/null 2>&1
rc-update add docker default >/dev/null 2>&1
service docker start >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
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
exit 1
fi
;;
"arch")
pacman -Sy docker docker-compose --noconfirm
systemctl enable docker.service
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Failed to install Docker with pacman. Try to install it manually."
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
systemctl enable docker.service >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
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
exit 1
fi
;;
"amzn")
dnf install docker -y
dnf install docker -y >/dev/null 2>&1
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
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
systemctl start docker
systemctl enable docker
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Failed to install Docker with dnf. Try to install it manually."
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
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
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
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
exit 1
fi
;;
*)
# Automated Docker installation
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed with Rancher script. Trying with official script."
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."
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker installation failed."
echo " Maybe your OS is not supported?"
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
fi
esac
echo " - Docker installed successfully."
else
echo " - Docker is installed."
fi
echo -e "-------------"
echo -e "Check Docker Configuration..."
echo -e "4. Check Docker Configuration. "
mkdir -p /etc/docker
# 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
@@ -277,34 +300,33 @@ fi
mv "$TEMP_FILE" /etc/docker/daemon.json
restart_docker_service() {
# Check if systemctl is available
if command -v systemctl >/dev/null 2>&1; then
echo "Using systemctl to restart Docker..."
echo " - Using systemctl to restart Docker."
systemctl restart docker
if [ $? -eq 0 ]; then
echo "Docker restarted successfully using systemctl."
echo " - Docker restarted successfully using systemctl."
else
echo "Failed to restart Docker using systemctl."
echo " - Failed to restart Docker using systemctl."
return 1
fi
# Check if service command is available
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
if [ $? -eq 0 ]; then
echo "Docker restarted successfully using service."
echo " - Docker restarted successfully using service."
else
echo "Failed to restart Docker using service."
echo " - Failed to restart Docker using service."
return 1
fi
# If neither systemctl nor service is available
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
fi
}
@@ -312,40 +334,30 @@ restart_docker_service() {
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"))
if [ "$DIFF" != "" ]; then
echo "Docker configuration updated, restart docker daemon..."
echo " - Docker configuration updated, restart docker daemon..."
restart_docker_service
else
echo "Docker configuration is up to date."
echo " - Docker configuration is up to date."
fi
else
echo "Docker configuration updated, restart docker daemon..."
echo " - Docker configuration updated, restart docker daemon..."
restart_docker_service
fi
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..."
echo -e "5. Download required files from CDN. "
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/.env.production -o /data/coolify/source/.env.production
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
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
else
echo "File does not exist: $ENV_FILE"
echo "Copying .env.production to .env-$DATE"
echo " - File does not exist: $ENV_FILE"
echo " - Copying .env.production to .env-$DATE"
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
# Generate a secure APP_ID and APP_KEY
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
@@ -366,6 +378,7 @@ else
fi
# 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
if [ "$AUTOUPDATE" = "false" ]; then
@@ -375,33 +388,122 @@ if [ "$AUTOUPDATE" = "false" ]; then
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
fi
fi
# 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
}
echo -e "8. Checking for SSH key for localhost access."
if [ ! -f ~/.ssh/authorized_keys ]; then
mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys
addSshKey
chmod 600 ~/.ssh/authorized_keys
fi
if ! grep -qw "root@coolify" ~/.ssh/authorized_keys; then
addSshKey
fi
checkSshKeyInAuthorizedKeys() {
grep -qw "root@coolify" ~/.ssh/authorized_keys
return $?
}
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
checkSshKeyInCoolifyData() {
[ -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
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
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 "\033[0;35m
____ _ _ _ _ _
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| |
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|___/
\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,7 +12,6 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
# 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
# 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
sed -i "s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g" /data/coolify/source/.env

View File

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

84
package-lock.json generated
View File

@@ -7,9 +7,15 @@
"dependencies": {
"@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.13",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"alpinejs": "3.14.0",
"cookie": "^0.6.0",
"dotenv": "^16.4.5",
"ioredis": "5.4.1",
"tailwindcss-scrollbar": "0.1.0"
"node-pty": "^1.0.0",
"tailwindcss-scrollbar": "0.1.0",
"ws": "^8.17.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "4.5.1",
@@ -20,7 +26,7 @@
"postcss": "8.4.38",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.4",
"vite": "4.5.3",
"vite": "4.5.5",
"vue": "3.4.29"
}
},
@@ -692,6 +698,19 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"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": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz",
@@ -940,6 +959,15 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -1000,6 +1028,18 @@
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"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": {
"version": "1.4.692",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz",
@@ -1475,6 +1515,11 @@
"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": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -1492,6 +1537,15 @@
"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": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -2028,9 +2082,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
"integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
@@ -2124,6 +2178,26 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"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": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",

View File

@@ -14,14 +14,20 @@
"postcss": "8.4.38",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.4",
"vite": "4.5.3",
"vite": "4.5.5",
"vue": "3.4.29"
},
"dependencies": {
"@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.13",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"alpinejs": "3.14.0",
"cookie": "^0.6.0",
"dotenv": "^16.4.5",
"ioredis": "5.4.1",
"tailwindcss-scrollbar": "0.1.0"
"node-pty": "^1.0.0",
"tailwindcss-scrollbar": "0.1.0",
"ws": "^8.17.0"
}
}

BIN
public/svgs/budge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -4,3 +4,18 @@
// const app = createApp({});
// app.component("magic-bar", MagicBar);
// 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,
name: 'Goto: Command Center',
name: 'Goto: Terminal',
icon: 'goto',
sequence: ['main', 'redirect']
},
@@ -653,7 +653,7 @@ async function redirect() {
targetUrl.pathname = `/settings`
break;
case 19:
targetUrl.pathname = `/command-center`
targetUrl.pathname = `/terminal`
break;
case 20:
targetUrl.pathname = `/team/notifications`

View File

@@ -1,6 +1,6 @@
<div class="w-full">
@if ($label)
<label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
<label class="flex gap-1 items-center mb-1 text-sm font-medium">{{ $label }}
@if ($required)
<x-highlighted text="*" />
@endif
@@ -9,7 +9,8 @@
@endif
</label>
@endif
<select {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
<select {{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
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 }}
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif>
{{ $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="{
switchWidth() {
if (this.full === 'full') {
localStorage.removeItem('pageWidth');
localStorage.setItem('pageWidth', 'center');
} else {
localStorage.setItem('pageWidth', 'full');
}
@@ -74,8 +74,10 @@
<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>
<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" x-show="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 === 'full'">Center</button>
<button @click="switchWidth()" class="px-1 dropdown-item-no-padding"
x-show="full === 'center'">Full</button>
</div>
</x-dropdown>
</div>
@@ -226,9 +228,9 @@
</a>
</li>
<li>
<a title="Command Center"
class="{{ request()->is('command-center*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('command-center') }}">
<a title="Terminal"
class="{{ request()->is('terminal*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('terminal') }}">
<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-linejoin="round">
@@ -236,7 +238,7 @@
<path d="M5 7l5 5l-5 5" />
<path d="M12 19l7 0" />
</svg>
Command Center
Terminal
</a>
</li>
<li>

View File

@@ -9,6 +9,10 @@
open: false,
init() {
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'">
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">

View File

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

View File

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

View File

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

View File

@@ -6,17 +6,21 @@
<livewire:activity-monitor header="Logs" showWaiting fullHeight />
</x-slot:content>
</x-slide-over>
<h1>Configuration</h1>
<h1>{{ $title }}</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data>
<nav class="flex items-center flex-shrink-0 gap-6 scrollbar min-h-10 whitespace-nowrap">
<nav class="flex flex-shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}"
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>
</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" />
</nav>
<div class="flex flex-wrap items-center order-first gap-2 sm:order-last">
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
@if (str($service->status())->contains('running'))
<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">

View File

@@ -4,57 +4,39 @@
</x-slot>
<livewire:project.shared.configuration-checker :resource="$resource" />
@if ($type === 'application')
<h1>Execute Command</h1>
<h1>Terminal</h1>
<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')
<h1>Execute Command</h1>
<h1>Terminal</h1>
<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')
<h2>Execute Command</h2>
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" title="Terminal" />
@endif
<div x-init="$wire.loadContainers">
<div class="pt-4" wire:loading wire:target='loadContainers'>
Loading containers...
Loading resources...
</div>
<div wire:loading.remove wire:target='loadContainers'>
@if (count($containers) > 0)
<form class="flex flex-col gap-2 pt-4" wire:submit='runCommand'>
<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>
<form class="flex flex-col gap-2 justify-center pt-4 xl:items-end xl:flex-row"
wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select label="Container" id="container" required>
<option disabled selected>Select container</option>
@if (data_get($this->parameters, 'application_uuid'))
@foreach ($containers as $container)
<option value="{{ data_get($container, 'container.Names') }}">
{{ 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>
@endif
</x-forms.select>
<x-forms.button type="submit">Run</x-forms.button>
<x-forms.button type="submit">Connect</x-forms.button>
</form>
@else
<div class="pt-4">No containers are not running.</div>
@endif
</div>
</div>
<div class="w-full pt-10 mx-auto">
<livewire:activity-monitor header="Command output" />
<div class="mx-auto w-full">
<livewire:project.shared.terminal />
</div>
</div>

View File

@@ -0,0 +1,240 @@
<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) {
const predefined = {
protocol: "{{ env('TERMINAL_PROTOCOL') }}",
host: "{{ env('TERMINAL_HOST') }}",
port: "{{ env('TERMINAL_PORT') }}"
}
const connectionString = {
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
host: window.location.hostname,
port: ":6002",
path: '/terminal/ws'
}
if (!window.location.port) {
connectionString.port = ''
}
if (predefined.host) {
connectionString.host = predefined.host
}
if (predefined.port) {
connectionString.port = `:${predefined.port}`
}
if (predefined.protocol) {
connectionString.protocol = predefined.protocol
}
const url =
`${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
socket = new WebSocket(url);
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

@@ -1,19 +0,0 @@
<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)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
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 10 minutes." />
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." />
@else
<x-forms.input id="server.settings.docker_cleanup_threshold"
label="Docker cleanup threshold (%)" required

View File

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

View File

@@ -0,0 +1,34 @@
<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,7 +6,6 @@ use App\Http\Controllers\OauthController;
use App\Http\Controllers\UploadController;
use App\Livewire\Admin\Index as AdminIndex;
use App\Livewire\Boarding\Index as BoardingIndex;
use App\Livewire\CommandCenter\Index as CommandCenterIndex;
use App\Livewire\Dashboard;
use App\Livewire\Dev\Compose as Compose;
use App\Livewire\ForcePasswordReset;
@@ -64,6 +63,7 @@ use App\Livewire\Tags\Show as TagsShow;
use App\Livewire\Team\AdminView as TeamAdminView;
use App\Livewire\Team\Index as TeamIndex;
use App\Livewire\Team\Member\Index as TeamMemberIndex;
use App\Livewire\Terminal\Index as TerminalIndex;
use App\Livewire\Waitlist\Index as WaitlistIndex;
use App\Models\GitlabApp;
use App\Models\PrivateKey;
@@ -153,7 +153,14 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/admin', TeamAdminView::class)->name('team.admin-view');
});
Route::get('/command-center', CommandCenterIndex::class)->name('command-center');
Route::get('/terminal', TerminalIndex::class)->name('terminal');
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::get('/{uuid}', [Controller::class, 'accept_invitation'])->name('team.invitation.accept');
@@ -176,20 +183,20 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/deployment', DeploymentIndex::class)->name('project.application.deployment.index');
Route::get('/deployment/{deployment_uuid}', DeploymentShow::class)->name('project.application.deployment.show');
Route::get('/logs', Logs::class)->name('project.application.logs');
Route::get('/command', ExecuteContainerCommand::class)->name('project.application.command');
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.application.command');
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::get('/', DatabaseConfiguration::class)->name('project.database.configuration');
Route::get('/logs', Logs::class)->name('project.database.logs');
Route::get('/command', ExecuteContainerCommand::class)->name('project.database.command');
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.database.command');
Route::get('/backups', DatabaseBackupIndex::class)->name('project.database.backup.index');
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::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('/command', ExecuteContainerCommand::class)->name('project.service.command');
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks');
});

View File

@@ -5,11 +5,30 @@ set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check
#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
CDN="https://cdn.coollabs.io/coolify"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.4"
VERSION="1.5"
DOCKER_VERSION="26.0"
CDN="https://cdn.coollabs.io/coolify"
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
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 '"')
ENV_FILE="/data/coolify/source/.env"
@@ -46,12 +65,16 @@ fi
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_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
if [ -z "$LATEST_HELPER_VERSION" ]; then
LATEST_HELPER_VERSION=latest
fi
DATE=$(date +"%Y%m%d-%H%M%S")
if [ -z "$LATEST_REALTIME_VERSION" ]; then
LATEST_REALTIME_VERSION=latest
fi
if [ $EUID != 0 ]; then
echo "Please run as root"
@@ -73,18 +96,29 @@ if [ "$1" != "" ]; then
LATEST_VERSION="${LATEST_VERSION#v}"
fi
echo -e "-------------"
echo -e "Welcome to Coolify v4 beta installer!"
echo -e "This script will install everything for you."
echo -e "\033[0;35m"
cat << "EOF"
_____ _ _ __
/ ____| | (_)/ _|
| | ___ ___ | |_| |_ _ _
| | / _ \ / _ \| | | _| | | |
| |___| (_) | (_) | | | | | |_| |
\_____\___/ \___/|_|_|_| \__, |
__/ |
|___/
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 "-------------"
echo "OS: $OS_TYPE $OS_VERSION"
echo "Coolify version: $LATEST_VERSION"
echo "Helper version: $LATEST_HELPER_VERSION"
echo -e "-------------"
echo "Installing required packages..."
echo -e "---------------------------------------------"
echo "| Operating System | $OS_TYPE $OS_VERSION"
echo "| Docker | $DOCKER_VERSION"
echo "| Coolify | $LATEST_VERSION"
echo "| Helper | $LATEST_HELPER_VERSION"
echo "| Realtime | $LATEST_REALTIME_VERSION"
echo -e "---------------------------------------------\n"
echo -e "1. Installing required packages (curl, wget, git, jq). "
case "$OS_TYPE" in
arch)
@@ -122,24 +156,26 @@ sles | opensuse-leap | opensuse-tumbleweed)
;;
esac
echo -e "2. Check OpenSSH server configuration. "
# Detect OpenSSH server
SSH_DETECTED=false
if [ -x "$(command -v systemctl)" ]; then
if systemctl status sshd >/dev/null 2>&1; then
echo "OpenSSH server is installed."
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
if systemctl status ssh >/dev/null 2>&1; then
echo "OpenSSH server is installed."
elif systemctl status ssh >/dev/null 2>&1; then
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
elif [ -x "$(command -v service)" ]; then
if service sshd status >/dev/null 2>&1; then
echo "OpenSSH server is installed."
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
if service ssh status >/dev/null 2>&1; then
echo "OpenSSH server is installed."
elif service ssh status >/dev/null 2>&1; then
echo " - OpenSSH server is installed."
SSH_DETECTED=true
fi
fi
@@ -151,104 +187,91 @@ if [ "$SSH_DETECTED" = "false" ]; then
fi
# Detect SSH PermitRootLogin
SSH_PERMIT_ROOT_LOGIN=false
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)"
if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "without-password" ]; then
echo "PermitRootLogin is enabled."
SSH_PERMIT_ROOT_LOGIN=true
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 "###############################################################################"
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then
echo " - SSH PermitRootLogin is enabled."
else
echo " - SSH PermitRootLogin is disabled."
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh"
fi
# Detect if docker is installed via snap
if [ -x "$(command -v snap)" ]; then
if snap list | grep -q docker; then
echo "Docker is installed via snap."
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false")
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then
echo " - Docker is 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."
exit 1
fi
fi
echo -e "3. Check Docker Installation. "
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker is not installed. Installing Docker. It may take a while."
getAJoke
case "$OS_TYPE" in
"almalinux")
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
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
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
fi
systemctl start docker
systemctl enable docker
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
;;
"alpine")
apk add docker docker-cli-compose
rc-update add docker default
service docker start
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Failed to install Docker with apk. Try to install it manually."
apk add docker docker-cli-compose >/dev/null 2>&1
rc-update add docker default >/dev/null 2>&1
service docker start >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
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
exit 1
fi
;;
"arch")
pacman -Sy docker docker-compose --noconfirm
systemctl enable docker.service
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Failed to install Docker with pacman. Try to install it manually."
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
systemctl enable docker.service >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
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
exit 1
fi
;;
"amzn")
dnf install docker -y
dnf install docker -y >/dev/null 2>&1
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
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
systemctl start docker
systemctl enable docker
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Failed to install Docker with dnf. Try to install it manually."
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
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
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
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
exit 1
fi
;;
*)
# Automated Docker installation
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed with Rancher script. Trying with official script."
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."
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker installation failed."
echo " Maybe your OS is not supported?"
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
fi
esac
echo " - Docker installed successfully."
else
echo " - Docker is installed."
fi
echo -e "-------------"
echo -e "Check Docker Configuration..."
echo -e "4. Check Docker Configuration. "
mkdir -p /etc/docker
# 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
@@ -277,34 +300,33 @@ fi
mv "$TEMP_FILE" /etc/docker/daemon.json
restart_docker_service() {
# Check if systemctl is available
if command -v systemctl >/dev/null 2>&1; then
echo "Using systemctl to restart Docker..."
echo " - Using systemctl to restart Docker."
systemctl restart docker
if [ $? -eq 0 ]; then
echo "Docker restarted successfully using systemctl."
echo " - Docker restarted successfully using systemctl."
else
echo "Failed to restart Docker using systemctl."
echo " - Failed to restart Docker using systemctl."
return 1
fi
# Check if service command is available
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
if [ $? -eq 0 ]; then
echo "Docker restarted successfully using service."
echo " - Docker restarted successfully using service."
else
echo "Failed to restart Docker using service."
echo " - Failed to restart Docker using service."
return 1
fi
# If neither systemctl nor service is available
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
fi
}
@@ -312,39 +334,30 @@ restart_docker_service() {
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"))
if [ "$DIFF" != "" ]; then
echo "Docker configuration updated, restart docker daemon..."
echo " - Docker configuration updated, restart docker daemon..."
restart_docker_service
else
echo "Docker configuration is up to date."
echo " - Docker configuration is up to date."
fi
else
echo "Docker configuration updated, restart docker daemon..."
echo " - Docker configuration updated, restart docker daemon..."
restart_docker_service
fi
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..."
echo -e "5. Download required files from CDN. "
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/.env.production -o /data/coolify/source/.env.production
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
if [ -f $ENV_FILE ]; then
echo "File exists: $ENV_FILE"
echo "Copying .env to .env-$DATE"
cp $ENV_FILE $ENV_FILE-$DATE
else
echo "File does not exist: $ENV_FILE"
echo "Copying .env.production to .env-$DATE"
echo " - File does not exist: $ENV_FILE"
echo " - Copying .env.production to .env-$DATE"
cp /data/coolify/source/.env.production $ENV_FILE-$DATE
# Generate a secure APP_ID and APP_KEY
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
@@ -365,7 +378,7 @@ else
fi
# Merge .env and .env.production. New values will be added to .env
echo "Updating .env with new values (if necessary)..."
echo -e "7. Propagating .env with new values - if necessary."
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE
if [ "$AUTOUPDATE" = "false" ]; then
@@ -375,37 +388,122 @@ if [ "$AUTOUPDATE" = "false" ]; then
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
fi
fi
# 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
}
echo -e "8. Checking for SSH key for localhost access."
if [ ! -f ~/.ssh/authorized_keys ]; then
mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys
addSshKey
chmod 600 ~/.ssh/authorized_keys
fi
if ! grep -qw "root@coolify" ~/.ssh/authorized_keys; then
addSshKey
fi
checkSshKeyInAuthorizedKeys() {
grep -qw "root@coolify" ~/.ssh/authorized_keys
return $?
}
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
checkSshKeyInCoolifyData() {
[ -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
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
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"
echo -e "\033[0;35m
____ _ _ _ _ _
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| |
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|___/
\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
echo -e "Your .env file has been copied to /data/coolify/source/.env.backup\n"

View File

@@ -1,6 +1,7 @@
# documentation: https://github.com/linuxserver/budge
# slogan: A budgeting personal finance app.
# tags: personal finance, budgeting, expense tracking
# logo: svgs/budge.png
services:
budge:

File diff suppressed because one or more lines are too long

View File

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