Merge branch 'next' into feat--terminal-pty

This commit is contained in:
Andras Bacsai
2024-09-11 10:41:33 +02:00
committed by GitHub
197 changed files with 8925 additions and 3563 deletions

View File

@@ -1,16 +1,34 @@
APP_NAME=Coolify-localhost
APP_ID=development
# Coolify Configuration
APP_ENV=local
APP_NAME="Coolify Development"
APP_ID=development
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_PORT=8000
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
## For Andras only
# To purge cache
# PostgreSQL Database Configuration
DB_DATABASE=coolify
DB_USERNAME=coolify
DB_PASSWORD=password
DB_HOST=host.docker.internal
DB_PORT=5432
# Ray Configuration
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
RAY_PORT=
# Special Keys for Andras
# For cache purging
BUNNY_API_KEY=
# To upload assets
# For asset uploads
BUNNY_STORAGE_API_KEY=

View File

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

View File

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

View File

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

View File

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

189
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,189 @@
# Contributing
> "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.
## Code Contribution
## 1. Setup your development environment
Follow the steps below for your operating system:
### Windows
1. Install `docker-ce`, Docker Desktop (or similar):
- Docker CE (recommended):
- Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install)
- After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/)
- Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
- Install Docker Desktop (easier):
- Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
- Ensure WSL2 backend is enabled in Docker Desktop settings
2. Install Spin:
- Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2)
### MacOS
1. Install Orbstack, Docker Desktop (or similar):
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
- Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation)
- Docker Desktop:
- Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
2. Install Spin:
- Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin)
### Linux
1. Install Docker Engine, Docker Desktop (or similar):
- Docker Engine (recommended, as there is no VM overhead):
- Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/) for your Linux distribution
- Docker Desktop:
- If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/)
2. Install Spin:
- Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions)
## 2. Verify installation (optional)
After installing Docker (or Orbstack) and Spin, verify the installation:
1. Open a terminal or command prompt
2. Run the following commands:
```bash
docker --version
spin --version
```
You should see version information for both Docker and Spin.
## 3. Fork the Coolify repository and setup your 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):
- 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
3. Clone the Coolify Repository from your fork to your local machine
- Use `git clone` in the command line
- Use GitHub Desktop (recommended):
- Download and install from [https://desktop.github.com/](https://desktop.github.com/)
- Open GitHub Desktop and login with your GitHub account
- Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
4. Open the cloned Coolify Repository in your chosen code editor.
## 4. Set up Environment Variables
1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
5. Save the changes to your `.env` file.
## 5. Start Coolify
1. Open a terminal in the local Coolify directory.
2. Run the following command in the terminal (leave that terminal open):
```
spin up
```
Note: You may see some errors, but don't worry; this is expected.
3. If you encounter permission errors, especially on macOS, use:
```
sudo spin up
```
Note: If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
## 6. Start Development
1. Access your Coolify instance:
- URL: `http://localhost:8000`
- Login: `test@example.com`
- Password: `password`
2. Additional development tools:
- 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
```
## 7. Development Notes
When working on Coolify, keep the following in mind:
1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
```bash
docker exec -it coolify php artisan migrate
```
2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
```bash
docker exec -it coolify php artisan migrate:fresh --seed
```
3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any envrionement 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.
## 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
1. After making changes or adding a new service:
- Commit your changes to your forked repository.
- Push the changes to your GitHub account.
2. Creating the Pull Request (PR):
- Navigate to the main Coolify repository on GitHub.
- Click the "Pull requests" tab.
- Click the green "New pull request" button.
- Choose your fork and branch as the compare branch.
- Click "Create pull request".
3. Filling out the PR details:
- Give your PR a descriptive title.
- In the description, explain the changes you've made.
- Reference any related issues by using keywords like "Fixes #123" or "Closes #456".
4. Important note:
Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
5. Submit your PR:
- Review your changes one last time.
- Click "Create pull request" to submit.
After submission, maintainers will review your PR and may request changes or provide feedback.

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ class StartDragonfly
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -75,18 +75,11 @@ class StartDragonfly
],
],
];
if (! is_null($this->database->limits_cpuset)) {
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -102,6 +95,11 @@ class StartDragonfly
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -120,10 +118,10 @@ class StartDragonfly
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -154,7 +152,7 @@ class StartDragonfly
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
}

View File

@@ -24,7 +24,7 @@ class StartKeydb
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -74,18 +74,11 @@ class StartKeydb
],
],
];
if (! is_null($this->database->limits_cpuset)) {
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -101,15 +94,19 @@ class StartKeydb
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/keydb.conf',
'source' => $this->configuration_dir . '/keydb.conf',
'target' => '/etc/keydb/keydb.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -128,10 +125,10 @@ class StartKeydb
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -162,10 +159,12 @@ class StartKeydb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -21,7 +21,7 @@ class StartMariadb
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,18 +69,11 @@ class StartMariadb
],
],
];
if (! is_null($this->database->limits_cpuset)) {
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -96,14 +89,19 @@ class StartMariadb
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-config.cnf',
'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -122,10 +120,10 @@ class StartMariadb
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -156,21 +154,23 @@ class StartMariadb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -23,7 +23,7 @@ class StartMongodb
$startCommand = 'mongod';
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -77,18 +77,11 @@ class StartMongodb
],
],
];
if (! is_null($this->database->limits_cpuset)) {
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -104,23 +97,27 @@ class StartMongodb
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/mongod.conf',
'source' => $this->configuration_dir . '/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
}
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -139,10 +136,10 @@ class StartMongodb
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -173,18 +170,20 @@ class StartMongodb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -21,7 +21,7 @@ class StartMysql
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,18 +69,11 @@ class StartMysql
],
],
];
if (! is_null($this->database->limits_cpuset)) {
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -96,14 +89,19 @@ class StartMysql
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/custom-config.cnf',
'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -122,10 +120,10 @@ class StartMysql
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -156,21 +154,23 @@ class StartMysql
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

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

View File

@@ -24,7 +24,7 @@ class StartRedis
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -78,18 +78,11 @@ class StartRedis
],
],
];
if (! is_null($this->database->limits_cpuset)) {
if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -105,15 +98,20 @@ class StartRedis
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir.'/redis.conf',
'source' => $this->configuration_dir . '/redis.conf',
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -132,10 +130,10 @@ class StartRedis
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -166,10 +164,12 @@ class StartRedis
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server;
use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,17 +10,30 @@ class CleanupDocker
{
use AsAction;
public function handle(Server $server, bool $force = true)
public function handle(Server $server)
{
// cleanup docker images, containers, and builder caches
if ($force) {
instant_remote_process(['docker image prune -af'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
instant_remote_process(['docker builder prune -af'], $server, false);
} else {
instant_remote_process(['docker image prune -f'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
instant_remote_process(['docker builder prune -f'], $server, false);
$commands = $this->getCommands();
foreach ($commands as $command) {
instant_remote_process([$command], $server, false);
}
}
private function getCommands(): array
{
$settings = InstanceSettings::get();
$helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('coolify.helper_image');
$helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion;
$commonCommands = [
'docker container prune -f --filter "label=coolify.managed=true"',
'docker image prune -af --filter "label!=coolify.managed=true"',
'docker builder prune -af',
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi",
];
return $commonCommands;
}
}

View File

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

View File

@@ -4,8 +4,6 @@ namespace App\Actions\Server;
use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -26,12 +24,7 @@ class UpdateCoolify
if (! $this->server) {
return;
}
CleanupDocker::dispatch($this->server, false)->onQueue('high');
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
}
CleanupDocker::dispatch($this->server)->onQueue('high');
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
if (! $manual_update) {
@@ -62,10 +55,11 @@ class UpdateCoolify
return;
}
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
remote_process([
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
], $this->server);
}
}

View File

@@ -16,8 +16,10 @@ class StartService
$service->saveComposeConfigs();
$commands[] = 'cd '.$service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
if($service->networks()->count() > 0){
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
}
$commands[] = 'echo Starting service.';
$commands[] = "echo 'Pulling images.'";
$commands[] = 'docker compose pull';

View File

@@ -19,14 +19,19 @@ class StopService
ray('Stopping service: '.$service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(command: ["docker stop --time=30 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
if ($applications->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
}
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
instant_remote_process(command: ["docker stop --time=30 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
if ($dbs->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
}
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
$db->update(['status' => 'exited']);

View File

@@ -3,6 +3,8 @@
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Service;
use App\Models\ServiceApplication;
@@ -42,6 +44,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$applicationsPreviews = ApplicationPreview::get();
foreach ($applicationsPreviews as $applicationPreview) {
if (! data_get($applicationPreview, 'application')) {
echo "Deleting stuck application preview: {$applicationPreview->uuid}\n";
$applicationPreview->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
@@ -153,6 +166,18 @@ class CleanupStuckedResources extends Command
echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n";
}
try {
$scheduled_backups = ScheduledDatabaseBackup::all();
foreach ($scheduled_backups as $scheduled_backup) {
if (! $scheduled_backup->server()) {
echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
$scheduled_backup->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck scheduledbackups: {$e->getMessage()}\n";
}
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();

View File

@@ -132,6 +132,9 @@ class Init extends Command
private function cleanup_unused_network_from_coolify_proxy()
{
if (isCloud()) {
return;
}
foreach ($this->servers as $server) {
if (! $server->isFunctional()) {
continue;

View File

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

View File

@@ -4,9 +4,9 @@ namespace App\Console;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullCoolifyImageJob;
use App\Jobs\PullHelperImageJob;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN;
@@ -30,22 +30,24 @@ class Kernel extends ConsoleKernel
$this->all_servers = Server::all();
$settings = InstanceSettings::get();
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_scheduled_tasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->command('telescope:prune')->daily();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('cleanup:unreachable-servers')->daily();
$schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->onOneServer();
$schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->schedule_updates($schedule);
@@ -66,9 +68,19 @@ class Kernel extends ConsoleKernel
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
$schedule->job(new PullSentinelImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
$schedule->job(function () use ($server) {
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status !== 'running') {
PullSentinelImageJob::dispatch($server);
}
$schedule->job(new PullHelperImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
$schedule->job(new PullHelperImageJob($server))
->cron($settings->update_check_frequency)
->timezone($settings->instance_timezone)
->onOneServer();
}
}
@@ -77,11 +89,17 @@ class Kernel extends ConsoleKernel
$settings = InstanceSettings::get();
$updateCheckFrequency = $settings->update_check_frequency;
$schedule->job(new CheckForUpdatesJob)->cron($updateCheckFrequency)->onOneServer();
$schedule->job(new CheckForUpdatesJob)
->cron($updateCheckFrequency)
->timezone($settings->instance_timezone)
->onOneServer();
if ($settings->is_auto_update_enabled) {
$autoUpdateFrequency = $settings->auto_update_frequency;
$schedule->job(new UpdateCoolifyJob)->cron($autoUpdateFrequency)->onOneServer();
$schedule->job(new UpdateCoolifyJob)
->cron($autoUpdateFrequency)
->timezone($settings->instance_timezone)
->onOneServer();
}
}
@@ -96,7 +114,12 @@ class Kernel extends ConsoleKernel
}
foreach ($servers as $server) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
$serverTimezone = $server->settings->server_timezone;
if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
}
}
}
@@ -117,12 +140,19 @@ class Kernel extends ConsoleKernel
continue;
}
$server = $scheduled_backup->server();
if (! $server) {
continue;
}
$serverTimezone = $server->settings->server_timezone;
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup
))->cron($scheduled_backup->frequency)->onOneServer();
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
}
}
@@ -155,12 +185,19 @@ class Kernel extends ConsoleKernel
continue;
}
}
$server = $scheduled_task->server();
if (! $server) {
continue;
}
$serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
$schedule->job(new ScheduledTaskJob(
task: $scheduled_task
))->cron($scheduled_task->frequency)->onOneServer();
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
}
}

View File

@@ -53,6 +53,7 @@ class ApplicationsController extends Controller
summary: 'List',
description: 'List all applications.',
path: '/applications',
operationId: 'list-applications',
security: [
['bearerAuth' => []],
],
@@ -101,6 +102,7 @@ class ApplicationsController extends Controller
summary: 'Create (Public)',
description: 'Create new application based on a public git repository.',
path: '/applications/public',
operationId: 'create-public-application',
security: [
['bearerAuth' => []],
],
@@ -202,6 +204,7 @@ class ApplicationsController extends Controller
summary: 'Create (Private - GH App)',
description: 'Create new application based on a private repository through a Github App.',
path: '/applications/private-github-app',
operationId: 'create-private-github-app-application',
security: [
['bearerAuth' => []],
],
@@ -303,6 +306,7 @@ class ApplicationsController extends Controller
summary: 'Create (Private - Deploy Key)',
description: 'Create new application based on a private repository through a Deploy Key.',
path: '/applications/private-deploy-key',
operationId: 'create-private-deploy-key-application',
security: [
['bearerAuth' => []],
],
@@ -404,6 +408,7 @@ class ApplicationsController extends Controller
summary: 'Create (Dockerfile)',
description: 'Create new application based on a simple Dockerfile.',
path: '/applications/dockerfile',
operationId: 'create-dockerfile-application',
security: [
['bearerAuth' => []],
],
@@ -490,6 +495,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Image)',
description: 'Create new application based on a prebuilt docker image',
path: '/applications/dockerimage',
operationId: 'create-dockerimage-application',
security: [
['bearerAuth' => []],
],
@@ -573,6 +579,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Compose)',
description: 'Create new application based on a docker-compose file.',
path: '/applications/dockercompose',
operationId: 'create-dockercompose-application',
security: [
['bearerAuth' => []],
],
@@ -1171,6 +1178,7 @@ class ApplicationsController extends Controller
summary: 'Get',
description: 'Get application by UUID.',
path: '/applications/{uuid}',
operationId: 'get-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1235,6 +1243,7 @@ class ApplicationsController extends Controller
summary: 'Delete',
description: 'Delete application by UUID.',
path: '/applications/{uuid}',
operationId: 'delete-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1321,6 +1330,7 @@ class ApplicationsController extends Controller
summary: 'Update',
description: 'Update application by UUID.',
path: '/applications/{uuid}',
operationId: 'update-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1557,6 +1567,7 @@ class ApplicationsController extends Controller
summary: 'List Envs',
description: 'List all envs by application UUID.',
path: '/applications/{uuid}/envs',
operationId: 'list-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1639,6 +1650,7 @@ class ApplicationsController extends Controller
summary: 'Update Env',
description: 'Update env by application UUID.',
path: '/applications/{uuid}/envs',
operationId: 'update-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1821,6 +1833,7 @@ class ApplicationsController extends Controller
summary: 'Update Envs (Bulk)',
description: 'Update multiple envs by application UUID.',
path: '/applications/{uuid}/envs/bulk',
operationId: 'update-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2012,6 +2025,7 @@ class ApplicationsController extends Controller
summary: 'Create Env',
description: 'Create env by application UUID.',
path: '/applications/{uuid}/envs',
operationId: 'create-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2171,6 +2185,7 @@ class ApplicationsController extends Controller
summary: 'Delete Env',
description: 'Delete env by UUID.',
path: '/applications/{uuid}/envs/{env_uuid}',
operationId: 'delete-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2256,6 +2271,7 @@ class ApplicationsController extends Controller
summary: 'Start',
description: 'Start application. `Post` request is also accepted.',
path: '/applications/{uuid}/start',
operationId: 'start-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2359,6 +2375,7 @@ class ApplicationsController extends Controller
summary: 'Stop',
description: 'Stop application. `Post` request is also accepted.',
path: '/applications/{uuid}/stop',
operationId: 'stop-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2431,6 +2448,7 @@ class ApplicationsController extends Controller
summary: 'Restart',
description: 'Restart application. `Post` request is also accepted.',
path: '/applications/{uuid}/restart',
operationId: 'restart-application-by-uuid',
security: [
['bearerAuth' => []],
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ use App\Models\ApplicationPreview;
use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\GitlabApp;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
@@ -109,10 +110,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $is_debug_enabled;
private $build_args;
private Collection|string $build_args;
private $env_args;
private $environment_variables;
private $env_nixpacks_args;
private $docker_compose;
@@ -166,6 +169,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
$this->build_args = collect([]);
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
@@ -204,7 +208,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}";
}
}
ray('New container name: ', $this->container_name);
ray('New container name: ', $this->container_name)->green();
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
@@ -419,15 +423,42 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
if ($this->preserveRepository) {
foreach ($this->application->fileStorages as $fileStorage) {
$path = $fileStorage->fs_path;
$saveName = 'file_stat_'.$fileStorage->id;
$realPathInGit = str($path)->replace($this->application->workdir(), $this->workdir)->value();
// check if the file is a directory or a file inside the repository
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "stat -c '%F' {$realPathInGit}"), 'hidden' => true, 'ignore_errors' => true, 'save' => $saveName]
);
if ($this->saved_outputs->has($saveName)) {
$fileStat = $this->saved_outputs->get($saveName);
if ($fileStat->value() === 'directory' && ! $fileStorage->is_directory) {
$fileStorage->is_directory = true;
$fileStorage->content = null;
$fileStorage->save();
$fileStorage->deleteStorageOnServer();
$fileStorage->saveStorageOnServer();
} elseif ($fileStat->value() === 'regular file' && $fileStorage->is_directory) {
$fileStorage->is_directory = false;
$fileStorage->is_based_on_git = true;
$fileStorage->save();
$fileStorage->deleteStorageOnServer();
$fileStorage->saveStorageOnServer();
}
}
}
}
$this->generate_image_names();
$this->cleanup_git();
$this->application->loadComposeFile(isInit: false);
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
$this->application->oldRawParser();
$yaml = $composeFile = $this->application->docker_compose_raw;
$this->save_environment_variables();
} else {
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id'));
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']);
@@ -444,11 +475,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$yaml = Yaml::dump($composeFile->toArray(), 10);
$yaml = Yaml::dump(convertToArray($composeFile), 10);
}
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
'hidden' => true,
]);
// Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -478,9 +510,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// TODO
} else {
$this->execute_remote_command([
"docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", 'hidden' => true, 'ignore_errors' => true,
"docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true",
'hidden' => true,
'ignore_errors' => true,
], [
"docker network connect {$networkId} coolify-proxy || true", 'hidden' => true, 'ignore_errors' => true,
"docker network connect {$networkId} coolify-proxy || true",
'hidden' => true,
'ignore_errors' => true,
]);
}
@@ -531,8 +567,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
$this->write_deployment_configurations();
}
}
}
@@ -626,20 +662,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
[
"mkdir -p $this->configuration_dir",
],
// removing this now as we are using docker cp
// [
// "rm -rf $this->configuration_dir/{*,.*}",
// ],
[
"docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}",
],
);
}
$this->application->fileStorages()->each(function ($fileStorage) {
foreach ($this->application->fileStorages as $fileStorage) {
if (! $fileStorage->is_based_on_git && ! $fileStorage->is_directory) {
$fileStorage->saveStorageOnServer();
}
});
}
if ($this->use_build_server) {
$this->server = $this->build_server;
}
@@ -719,7 +751,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"),
'hidden' => true,
],
);
if ($this->application->docker_registry_image_tag) {
@@ -727,10 +760,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
'ignore_errors' => true,
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
'ignore_errors' => true,
'hidden' => true,
],
);
}
@@ -827,14 +864,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function check_image_locally_or_remotely()
{
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
"docker images -q {$this->production_image_name} 2>/dev/null",
'hidden' => true,
'save' => 'local_image_found',
]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
"docker pull {$this->production_image_name} 2>/dev/null", 'ignore_errors' => true, 'hidden' => true,
"docker pull {$this->production_image_name} 2>/dev/null",
'ignore_errors' => true,
'hidden' => true,
]);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
"docker images -q {$this->production_image_name} 2>/dev/null",
'hidden' => true,
'save' => 'local_image_found',
]);
}
}
@@ -867,17 +910,24 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
$envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
$envs->push("COOLIFY_DOMAIN_FQDN={$url}");
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
}
}
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview);
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -912,18 +962,31 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
if ($this->application->compose_parsing_version === '3') {
$envs->push("COOLIFY_URL={$this->application->fqdn}");
} else {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
if ($this->application->compose_parsing_version === '3') {
$envs->push("COOLIFY_FQDN={$url}");
} else {
$envs->push("COOLIFY_URL={$url}");
}
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
}
}
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables);
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -1000,17 +1063,58 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
}
}
$this->environment_variables = $envs;
}
private function elixir_finetunes()
{
if ($this->pull_request_id === 0) {
$envType = 'environment_variables';
} else {
$envType = 'environment_variables_preview';
}
$mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first();
if ($mix_env) {
if ($mix_env->is_build_time === false) {
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
}
} else {
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
}
$secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first();
if ($secret_key_base) {
if ($secret_key_base->is_build_time === false) {
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
}
} else {
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
}
$database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first();
if ($database_url) {
if ($database_url->is_build_time === false) {
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
}
} else {
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
}
}
private function laravel_finetunes()
{
if ($this->pull_request_id === 0) {
$nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
$envType = 'environment_variables';
} else {
$nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
$envType = 'environment_variables_preview';
}
$nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
if (! $nixpacks_php_fallback_path) {
$nixpacks_php_fallback_path = new EnvironmentVariable;
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
@@ -1230,7 +1334,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
$settings = InstanceSettings::get();
$helperImage = config('coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
@@ -1382,7 +1488,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
$this->execute_remote_command(
[
$importCommands, 'hidden' => true,
$importCommands,
'hidden' => true,
]
);
$this->create_workdir();
@@ -1466,6 +1573,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value);
data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value);
}
if ($this->nixpacks_type === 'elixir') {
$this->elixir_finetunes();
}
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
if ($this->nixpacks_type === 'rust') {
@@ -1592,7 +1702,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Check for custom HEALTHCHECK
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true,
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
'hidden' => true,
'save' => 'dockerfile_from_repo',
'ignore_errors' => true,
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$this->application->parseHealthcheckFromDockerfile($dockerfile);
@@ -1695,14 +1808,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$this->container_name]['logging'] = generate_fluentd_configuration();
}
if ($this->application->settings->is_gpu_enabled) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
@@ -1865,13 +1971,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker pull {$image}"),
'hidden' => true,
]
);
}
private function build_image()
{
// Add Coolify related variables to the build args
$this->environment_variables->filter(function ($key, $value) {
return str($key)->startsWith('COOLIFY_');
})->each(function ($key, $value) {
$this->build_args->push("--build-arg '{$key}'");
});
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
@@ -1915,12 +2031,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
}
@@ -1928,10 +2046,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -1945,10 +2065,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
}
@@ -1985,10 +2107,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"),
],
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
} else {
@@ -2002,10 +2126,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
} else {
@@ -2014,22 +2140,26 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
}
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -2043,10 +2173,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
}
@@ -2072,7 +2204,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
private function stop_running_container(bool $force = false)
@@ -2142,15 +2273,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->build_args->push("--build-arg {$env->key}={$value}");
}
}
$this->build_args = $this->build_args->implode(' ');
ray($this->build_args);
}
private function add_build_env_variables_to_dockerfile()
{
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile',
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
'hidden' => true,
'save' => 'dockerfile',
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
@@ -2168,7 +2298,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
@@ -2196,7 +2325,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$exec = "docker exec {$containerName} {$cmd}";
$this->execute_remote_command(
[
'command' => $exec, 'hidden' => true,
'command' => $exec,
'hidden' => true,
],
);
@@ -2223,7 +2353,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
try {
$this->execute_remote_command(
[
'command' => $exec, 'hidden' => true, 'save' => 'post-deployment-command-output',
'command' => $exec,
'hidden' => true,
'save' => 'post-deployment-command-output',
],
);
} catch (Exception $e) {

View File

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

View File

@@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\File;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -25,12 +26,14 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version');
$current_version = config('version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
$settings->update(['new_version_available' => true]);
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
} else {
$settings->update(['new_version_available' => false]);
}

View File

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

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Process;
class CleanupStaleMultiplexedConnections implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
Server::chunk(100, function ($servers) {
foreach ($servers as $server) {
$this->cleanupStaleConnection($server);
}
});
}
private function cleanupStaleConnection(Server $server)
{
$muxSocket = "/tmp/mux_{$server->id}";
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
$checkProcess = Process::run($checkCommand);
if ($checkProcess->exitCode() !== 0) {
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
Process::run($closeCommand);
}
}
}

View File

@@ -25,6 +25,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use App\Models\InstanceSettings;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -56,6 +57,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $backup_output = null;
public ?string $postgres_password = null;
public ?S3Storage $s3 = null;
public function __construct($backup)
@@ -89,8 +92,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
BackupCreated::dispatch($this->team->id);
// Check if team is exists
if (is_null($this->team)) {
$this->backup->update(['status' => 'failed']);
@@ -99,6 +100,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
BackupCreated::dispatch($this->team->id);
$status = str(data_get($this->database, 'status'));
if (! $status->startsWith('running') && $this->database->id !== 0) {
ray('database not running');
@@ -134,6 +138,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$databasesToBackup = $this->database->postgres_user;
}
$this->postgres_password = $envs->filter(function ($env) {
return str($env)->startsWith('POSTGRES_PASSWORD=');
})->first();
if ($this->postgres_password) {
$this->postgres_password = str($this->postgres_password)->after('POSTGRES_PASSWORD=')->value();
}
} elseif (str($databaseType)->contains('mysql')) {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName.'-'.$this->container_name;
@@ -381,7 +392,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$backupCommand = 'docker exec';
if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password";
}
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$commands[] = $backupCommand;
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -452,7 +469,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success');
} else {
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
$deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1);
}
foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server);
@@ -477,12 +494,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$network = $this->database->destination->network;
}
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName();
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
$this->add_to_backup_output('Uploaded to S3.');
ray('Uploaded to S3. '.$this->backup_location.' to s3://'.$bucket.$this->backup_dir);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
throw $e;
@@ -491,4 +511,40 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
instant_remote_process([$command], $this->server);
}
}
private function ensureHelperImageAvailable(): void
{
$fullImageName = $this->getFullImageName();
$imageExists = $this->checkImageExists($fullImageName);
if (!$imageExists) {
$this->pullHelperImage($fullImageName);
}
}
private function checkImageExists(string $fullImageName): bool
{
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
return trim($result) === 'exists';
}
private function pullHelperImage(string $fullImageName): void
{
try {
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
} catch (\Exception $e) {
$errorMessage = "Failed to pull helper image: " . $e->getMessage();
$this->add_to_backup_output($errorMessage);
throw new \RuntimeException($errorMessage);
}
}
private function getFullImageName(): string
{
$settings = InstanceSettings::get();
$helperImage = config('coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";
}
}

View File

@@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
@@ -17,21 +18,33 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 300;
public $timeout = 600;
public int|string|null $usageBefore = null;
public $tries = 1;
public ?string $usageBefore = null;
public function __construct(public Server $server) {}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->id)];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle(): void
{
try {
if (! $this->server->isFunctional()) {
return;
}
if ($this->server->settings->is_force_cleanup_enabled) {
if ($this->server->settings->force_docker_cleanup) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server, force: true);
CleanupDocker::run(server: $this->server);
return;
}
@@ -39,12 +52,12 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
$this->usageBefore = $this->server->getDiskUsage();
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server, force: true);
CleanupDocker::run(server: $this->server);
return;
}
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
CleanupDocker::run(server: $this->server, force: false);
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.'));
@@ -56,7 +69,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
Log::info('No need to clean up '.$this->server->name);
}
} catch (\Throwable $e) {
ray($e->getMessage());
CleanupDocker::run(server: $this->server);
Log::error('DockerCleanupJob failed: '.$e->getMessage());
throw $e;
}
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -10,6 +11,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -32,10 +34,20 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
$settings = InstanceSettings::get();
$latest_version = data_get($versions, 'coolify.helper.version');
$current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) {
// New version available
$helperImage = config('coolify.helper_image');
ray("Pulling {$helperImage}");
instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false);
ray('PullHelperImageJob done');
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
$settings->update(['helper_version' => $latest_version]);
}
}
} catch (\Throwable $e) {
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
ray($e->getMessage());

View File

@@ -36,6 +36,8 @@ class ScheduledTaskJob implements ShouldQueue
public array $containers = [];
public string $server_timezone;
public function __construct($task)
{
$this->task = $task;
@@ -47,6 +49,19 @@ class ScheduledTaskJob implements ShouldQueue
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
$this->team = Team::find($task->team_id);
$this->server_timezone = $this->getServerTimezone();
}
private function getServerTimezone(): string
{
if ($this->resource instanceof Application) {
$timezone = $this->resource->destination->server->settings->server_timezone;
return $timezone;
} elseif ($this->resource instanceof Service) {
$timezone = $this->resource->server->settings->server_timezone;
return $timezone;
}
return 'UTC';
}
public function middleware(): array
@@ -61,6 +76,7 @@ class ScheduledTaskJob implements ShouldQueue
public function handle(): void
{
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
@@ -78,12 +94,12 @@ class ScheduledTaskJob implements ShouldQueue
} elseif ($this->resource->type() == 'service') {
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
$this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid');
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
$this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid');
$this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
}
@@ -96,8 +112,8 @@ class ScheduledTaskJob implements ShouldQueue
}
foreach ($this->containers as $containerName) {
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
$cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
$cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
$exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([
@@ -121,6 +137,7 @@ class ScheduledTaskJob implements ShouldQueue
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
} finally {
}
}
}

View File

@@ -26,6 +26,8 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public $tries = 3;
public $timeout = 60;
public $containers;
public $applications;
@@ -43,15 +45,15 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
// public function middleware(): array
// {
// return [(new WithoutOverlapping($this->server->uuid))];
// }
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
// public function uniqueId(): int
// {
// return $this->server->uuid;
// }
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle()
{
@@ -79,7 +81,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
$this->checkLogDrainContainer();
$this->checkSentinel();
}
} catch (\Throwable $e) {
@@ -90,21 +91,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
private function checkSentinel()
{
if ($this->server->isSentinelEnabled()) {
$sentinelContainerFound = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-sentinel';
})->first();
if ($sentinelContainerFound) {
$status = data_get($sentinelContainerFound, 'State.Status');
if ($status !== 'running') {
PullSentinelImageJob::dispatch($this);
}
}
}
}
private function serverStatus()
{
['uptime' => $uptime] = $this->server->validateConnection();
@@ -140,6 +126,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
private function checkLogDrainContainer()
{
if (! $this->server->isLogDrainEnabled()) {
return;
}
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();

View File

@@ -73,6 +73,8 @@ class Index extends Component
}
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
$this->remoteServerPort = $this->remoteServerPort;
$this->remoteServerUser = $this->remoteServerUser;
if (isDev()) {
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
@@ -154,6 +156,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->servers->count() > 0) {
$this->selectedExistingServer = $this->servers->first()->id;
$this->updateServerDetails();
$this->currentState = 'select-existing-server';
return;
@@ -173,9 +176,18 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->updateServerDetails();
$this->currentState = 'validate-server';
}
private function updateServerDetails()
{
if ($this->createdServer) {
$this->remoteServerPort = $this->createdServer->port;
$this->remoteServerUser = $this->createdServer->user;
}
}
public function getProxyType()
{
// Set Default Proxy Type
@@ -235,11 +247,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function saveServer()
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required',
'remoteServerName' => 'required|string',
'remoteServerHost' => 'required|string',
'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required',
'remoteServerUser' => 'required|string',
]);
$this->privateKey = formatPrivateKey($this->privateKey);
$foundServer = Server::whereIp($this->remoteServerHost)->first();
if ($foundServer) {
@@ -277,9 +290,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->settings()->update([
'is_reachable' => true,
]);
$this->serverReachable = true;
} catch (\Throwable $e) {
$this->serverReachable = false;
$this->createdServer->delete();
$this->createdServer->settings()->update([
'is_reachable' => false,
]);
return handleError(error: $e, livewire: $this);
}
@@ -296,6 +312,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->getProxyType();
} catch (\Throwable $e) {
$this->createdServer->settings()->update([
'is_usable' => false,
]);
return handleError(error: $e, livewire: $this);
}
}
@@ -349,6 +369,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
);
}
public function saveAndValidateServer()
{
$this->validate([
'remoteServerPort' => 'required|integer|min:1|max:65535',
'remoteServerUser' => 'required|string',
]);
$this->createdServer->update([
'port' => $this->remoteServerPort,
'user' => $this->remoteServerUser,
'timezone' => 'UTC',
]);
$this->validateServer();
}
private function createNewPrivateKey()
{
$this->privateKeyName = generate_random_name();

View File

@@ -50,15 +50,6 @@ class Dashboard extends Component
])->sortBy('id')->groupBy('server_name')->toArray();
}
// public function getIptables()
// {
// $servers = Server::ownedByCurrentTeam()->get();
// foreach ($servers as $server) {
// checkRequiredCommands($server);
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
// ray($iptables);
// }
// }
public function render()
{
return view('livewire.dashboard');

View File

@@ -66,9 +66,9 @@ class Advanced extends Component
$this->dispatch('resetDefaultLabels', false);
}
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
$this->application->oldRawParser();
} else {
$this->application->parseCompose();
$this->application->parse();
}
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
@@ -69,6 +70,20 @@ class Show extends Component
}
}
public function getLogLinesProperty()
{
return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) {
$logLine['line'] = e($logLine['line']);
$logLine['line'] = preg_replace(
'/(https?:\/\/[^\s]+)/',
'<a href="$1" target="_blank" rel="noopener noreferrer" class="underline text-neutral-400">$1</a>',
$logLine['line'],
);
return $logLine;
});
}
public function render()
{
return view('livewire.project.application.deployment.show');

View File

@@ -131,7 +131,7 @@ class General extends Component
public function mount()
{
try {
$this->parsedServices = $this->application->parseCompose();
$this->parsedServices = $this->application->parse();
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
@@ -196,7 +196,7 @@ class General extends Component
// Must reload the application to get the latest database changes
// Why? Not sure, but it works.
$this->application->refresh();
// $this->application->refresh();
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
if (is_null($this->parsedServices)) {
@@ -204,7 +204,7 @@ class General extends Component
return;
}
$compose = $this->application->parseCompose();
$this->application->parse();
$this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
$this->dispatch('refreshStorages');

View File

@@ -79,8 +79,15 @@ class Previews extends Component
return;
}
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
if ($this->application->build_pack === 'dockercompose') {
$preview->generate_preview_fqdn_compose();
$this->application->refresh();
$this->dispatch('success', 'Domain generated.');
return;
}
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
$url = Url::fromString($fqdn);
$template = $this->application->preview_url_template;
$host = $url->getHost();

View File

@@ -8,9 +8,8 @@ use Livewire\Component;
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
public $database;
public $executions = [];
public $setDeletableBackup;
public function getListeners()
@@ -58,7 +57,53 @@ class BackupExecutions extends Component
public function refreshBackupExecutions(): void
{
if ($this->backup) {
$this->executions = $this->backup->executions()->get()->sortBy('created_at');
$this->executions = $this->backup->executions()->get();
}
}
public function mount(ScheduledDatabaseBackup $backup)
{
$this->backup = $backup;
$this->database = $backup->database;
$this->refreshBackupExecutions();
}
public function server()
{
if ($this->database) {
$server = null;
if ($this->database instanceof \App\Models\ServiceDatabase) {
$server = $this->database->service->destination->server;
} elseif ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
}
if ($server) {
return $server;
}
}
return null;
}
public function getServerTimezone()
{
$server = $this->server();
if (!$server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
public function formatDateInServerTimezone($date)
{
$serverTimezone = $this->getServerTimezone();
$dateObj = new \DateTime($date);
try {
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
}

View File

@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -54,7 +56,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -71,14 +73,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -93,7 +95,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}

View File

@@ -30,6 +30,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -40,6 +41,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -52,7 +54,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -86,14 +88,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -108,7 +110,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}

View File

@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -55,7 +57,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -92,14 +94,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -114,7 +116,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}

View File

@@ -34,6 +34,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -48,6 +49,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Options',
];
public function mount()
@@ -61,7 +63,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -98,14 +100,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -120,7 +122,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}

View File

@@ -33,6 +33,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -46,6 +47,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -59,7 +61,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -99,14 +101,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -121,7 +123,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}

View File

@@ -34,6 +34,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -48,6 +49,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -60,7 +62,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -97,14 +99,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -119,7 +121,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}

View File

@@ -49,6 +49,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -65,6 +66,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()

View File

@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Options',
];
public function mount()
@@ -55,7 +57,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -86,14 +88,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && ! $this->database->public_port) {
if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (! str($this->database->status)->startsWith('running')) {
if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -108,7 +110,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}

View File

@@ -99,6 +99,16 @@ class PublicGitRepository extends Component
}
}
public function updatedDockerComposeLocation()
{
if ($this->docker_compose_location) {
$this->docker_compose_location = rtrim($this->docker_compose_location, '/');
if (! str($this->docker_compose_location)->startsWith('/')) {
$this->docker_compose_location = '/'.$this->docker_compose_location;
}
}
}
public function updatedBuildPack()
{
if ($this->build_pack === 'nixpacks') {

View File

@@ -45,6 +45,8 @@ class Select extends Component
public ?string $selectedEnvironment = null;
public string $postgresql_type = 'postgres:16-alpine';
public ?string $existingPostgresqlUrl = null;
public ?string $search = null;
@@ -202,6 +204,8 @@ class Select extends Component
$docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
if ($docker) {
$this->setDestination($docker->uuid);
return $this->whatToDoNext();
}
}
$this->current_step = 'destinations';
@@ -211,6 +215,28 @@ class Select extends Component
{
$this->destination_uuid = $destination_uuid;
return $this->whatToDoNext();
}
public function setPostgresqlType(string $type)
{
$this->postgresql_type = $type;
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
'type' => $this->type,
'destination' => $this->destination_uuid,
'server_id' => $this->server_id,
'database_image' => $this->postgresql_type,
]);
}
public function whatToDoNext()
{
if ($this->type === 'postgresql') {
$this->current_step = 'select-postgresql-type';
} else {
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
@@ -219,6 +245,7 @@ class Select extends Component
'server_id' => $this->server_id,
]);
}
}
public function loadServers()
{

View File

@@ -18,6 +18,7 @@ class Create extends Component
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
$database_image = request()->query('database_image');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
@@ -33,7 +34,11 @@ class Create extends Component
if (in_array($type, DATABASE_TYPES)) {
if ($type->value() === 'postgresql') {
$database = create_standalone_postgresql($environment->id, $destination_uuid);
$database = create_standalone_postgresql(
environmentId: $environment->id,
destinationUuid: $destination_uuid,
databaseImage: $database_image
);
} elseif ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
} elseif ($type->value() === 'mongodb') {
@@ -86,18 +91,16 @@ class Create extends Component
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$generatedValue = generateEnvValue($command->value(), $service);
}
if ($value) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'value' => $value,
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
}
});
}
$service->parse(isNew: true);

View File

@@ -76,6 +76,12 @@ class Configuration extends Component
{
try {
GetContainersStatus::run($this->service->server);
$this->service->applications->each(function ($application) {
$application->refresh();
});
$this->service->databases->each(function ($database) {
$database->refresh();
});
$this->dispatch('$refresh');
} catch (\Exception $e) {
return handleError($e, $this);

View File

@@ -43,6 +43,7 @@ class EditCompose extends Component
{
$this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
$this->dispatch('refreshStorages');
}
public function instantSave()

View File

@@ -55,6 +55,7 @@ class FileStorage extends Component
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->is_directory = true;
$this->fileStorage->content = null;
$this->fileStorage->is_based_on_git = false;
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
} catch (\Throwable $e) {

View File

@@ -24,6 +24,7 @@ class All extends Component
protected $listeners = [
'saveKey' => 'submit',
'refreshEnvs',
'environmentVariableDeleted' => 'refreshEnvs',
];
@@ -52,11 +53,18 @@ class All extends Component
public function sortEnvironmentVariables()
{
if ($this->resource->type() === 'application') {
$this->resource->load(['environment_variables', 'environment_variables_preview']);
} else {
$this->resource->load(['environment_variables']);
}
$sortBy = data_get($this->resource, 'settings.is_env_sorting_enabled') ? 'key' : 'order';
$sortFunction = function ($variables) use ($sortBy) {
if (! $variables) {
return $variables;
}
if ($sortBy === 'key') {
return $variables->sortBy(function ($item) {
return strtolower($item->key);

View File

@@ -97,7 +97,7 @@ class GetLogs extends Component
if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
return;
}
if (! $this->numberOfLines) {
if ($this->numberOfLines <= 0) {
$this->numberOfLines = 1000;
}
if ($this->container) {

View File

@@ -26,7 +26,7 @@ class All extends Component
$this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
} elseif ($this->resource->type() == 'application') {
if ($this->resource->build_pack === 'dockercompose') {
$parsed = $this->resource->parseCompose();
$parsed = $this->resource->parse();
$containers = collect(data_get($parsed, 'services'))->keys();
$this->containerNames = $containers;
} else {

View File

@@ -7,8 +7,8 @@ use Livewire\Component;
class Executions extends Component
{
public $executions = [];
public $selectedKey;
public $task;
public function getListeners()
{
@@ -26,4 +26,44 @@ class Executions extends Component
}
$this->selectedKey = $key;
}
public function server()
{
if (!$this->task) {
return null;
}
if ($this->task->application) {
if ($this->task->application->destination && $this->task->application->destination->server) {
return $this->task->application->destination->server;
}
} elseif ($this->task->service) {
if ($this->task->service->destination && $this->task->service->destination->server) {
return $this->task->service->destination->server;
}
}
return null;
}
public function getServerTimezone()
{
$server = $this->server();
if (!$server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
public function formatDateInServerTimezone($date)
{
$serverTimezone = $this->getServerTimezone();
$dateObj = new \DateTime($date);
try {
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
}

View File

@@ -18,12 +18,12 @@ class Form extends Component
public ?string $wildcard_domain = null;
public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false;
public bool $revalidate = false;
public $timezones;
protected $listeners = ['serverInstalled', 'revalidate' => '$refresh'];
protected $rules = [
@@ -37,7 +37,6 @@ class Form extends Component
'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean',
'server.settings.is_force_cleanup_enabled' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.is_metrics_enabled' => 'required|boolean',
@@ -46,6 +45,10 @@ class Form extends Component
'server.settings.metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url',
'server.settings.is_server_api_enabled' => 'required|boolean',
'server.settings.server_timezone' => 'required|string|timezone',
'server.settings.force_docker_cleanup' => 'required|boolean',
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
];
protected $validationAttributes = [
@@ -66,12 +69,27 @@ class Form extends Component
'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API',
'server.settings.server_timezone' => 'Server Timezone',
];
public function mount()
public function mount(Server $server)
{
$this->server = $server;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
}
public function updated($field)
{
if ($field === 'server.settings.docker_cleanup_frequency') {
$frequency = $this->server->settings->docker_cleanup_frequency;
if (empty($frequency) || ! validate_cron_expression($frequency)) {
$this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
}
}
}
public function serverInstalled()
@@ -116,7 +134,6 @@ class Form extends Component
}
if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) {
ray('Starting sentinel');
}
} else {
ray('Sentinel is not enabled');
@@ -172,6 +189,7 @@ class Form extends Component
public function submit()
{
try {
if (isCloud() && ! isDev()) {
$this->validate();
$this->validate([
@@ -190,9 +208,30 @@ class Form extends Component
}
refresh_server_connection($this->server->privateKey);
$this->server->settings->wildcard_domain = $this->wildcard_domain;
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
if ($this->server->settings->force_docker_cleanup) {
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
} else {
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
}
$currentTimezone = $this->server->settings->getOriginal('server_timezone');
$newTimezone = $this->server->settings->server_timezone;
if ($currentTimezone !== $newTimezone || $currentTimezone === '') {
$this->server->settings->server_timezone = $newTimezone;
$this->server->settings->save();
}
$this->server->settings->save();
$this->server->save();
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updatedServerSettingsServerTimezone($value)
{
$this->server->settings->server_timezone = $value;
$this->server->settings->save();
$this->dispatch('success', 'Server timezone updated.');
}
}

View File

@@ -40,6 +40,7 @@ class Index extends Component
'settings.is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
'settings.instance_timezone' => 'required|string|timezone',
];
protected $validationAttributes = [
@@ -54,6 +55,8 @@ class Index extends Component
'update_check_frequency' => 'Update Check Frequency',
];
public $timezones;
public function mount()
{
if (isInstanceAdmin()) {
@@ -65,6 +68,7 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
} else {
return redirect()->route('dashboard');
}
@@ -166,6 +170,13 @@ class Index extends Component
}
}
public function updatedSettingsInstanceTimezone($value)
{
$this->settings->instance_timezone = $value;
$this->settings->save();
$this->dispatch('success', 'Instance timezone updated.');
}
public function render()
{
return view('livewire.settings.index');

View File

@@ -4,7 +4,6 @@ namespace App\Livewire;
use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Upgrade extends Component
@@ -22,13 +21,8 @@ class Upgrade extends Component
public function checkUpdate()
{
try {
$settings = InstanceSettings::get();
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
$this->latestVersion = data_get($versions, 'coolify.v4.version');
}
$this->isUpgradeAvailable = $settings->new_version_available;
$this->latestVersion = get_latest_version_of_coolify();
$this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -102,6 +102,8 @@ class Application extends BaseModel
{
use SoftDeletes;
private static $parserVersion = '3';
protected $guarded = [];
protected $appends = ['server_status'];
@@ -125,7 +127,7 @@ class Application extends BaseModel
ApplicationSetting::create([
'application_id' => $application->id,
]);
$application->compose_parsing_version = '2';
$application->compose_parsing_version = self::$parserVersion;
$application->save();
});
static::forceDeleting(function ($application) {
@@ -138,6 +140,7 @@ class Application extends BaseModel
$task->delete();
}
$application->tags()->detach();
$application->previews()->delete();
});
}
@@ -412,23 +415,6 @@ class Application extends BaseModel
);
}
public function dockerComposePrLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/docker-compose.yaml';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function baseDirectory(): Attribute
{
return Attribute::make(
@@ -1040,7 +1026,7 @@ class Application extends BaseModel
}
}
public function parseRawCompose()
public function oldRawParser()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -1100,9 +1086,11 @@ class Application extends BaseModel
instant_remote_process($commands, $this->destination->server, false);
}
public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null)
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{
if ($this->docker_compose_raw) {
if ($this->compose_parsing_version === '3') {
return newParser($this, $pull_request_id, $preview_id);
} elseif ($this->docker_compose_raw) {
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
} else {
return collect([]);
@@ -1154,7 +1142,7 @@ class Application extends BaseModel
if ($composeFileContent) {
$this->docker_compose_raw = $composeFileContent;
$this->save();
$parsedServices = $this->parseCompose();
$parsedServices = $this->parse();
if ($this->docker_compose_domains) {
$json = collect(json_decode($this->docker_compose_domains));
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();

View File

@@ -12,9 +12,9 @@ class ApplicationPreview extends BaseModel
protected static function booted()
{
static::deleting(function ($preview) {
if ($preview->application->build_pack === 'dockercompose') {
if (data_get($preview, 'application.build_pack') === 'dockercompose') {
$server = $preview->application->destination->server;
$composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
$composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id);
$volumes = data_get($composeFile, 'volumes');
$networks = data_get($composeFile, 'networks');
$networkKeys = collect($networks)->keys();

View File

@@ -37,6 +37,30 @@ class InstanceSettings extends Model implements SendsEmail
);
}
public function updateCheckFrequency(): Attribute
{
return Attribute::make(
set: function ($value) {
return translate_cron_expression($value);
},
get: function ($value) {
return translate_cron_expression($value);
}
);
}
public function autoUpdateFrequency(): Attribute
{
return Attribute::make(
set: function ($value) {
return translate_cron_expression($value);
},
get: function ($value) {
return translate_cron_expression($value);
}
);
}
public static function get()
{
return InstanceSettings::findOrFail(0);

View File

@@ -52,6 +52,7 @@ class LocalFileVolume extends BaseModel
public function deleteStorageOnServer()
{
$this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();

View File

@@ -11,6 +11,7 @@ use OpenApi\Attributes as OA;
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'environments' => new OA\Property(
property: 'environments',
type: 'array',

View File

@@ -22,7 +22,8 @@ class ScheduledDatabaseBackup extends BaseModel
public function executions(): HasMany
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class);
// Last execution first
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc');
}
public function s3()
@@ -34,4 +35,14 @@ class ScheduledDatabaseBackup extends BaseModel
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
}
public function server()
{
if ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
return $server;
}
}
return null;
}
}

View File

@@ -4,6 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use App\Models\Service;
use App\Models\Application;
class ScheduledTask extends BaseModel
{
@@ -26,6 +28,28 @@ class ScheduledTask extends BaseModel
public function executions(): HasMany
{
return $this->hasMany(ScheduledTaskExecution::class);
// Last execution first
return $this->hasMany(ScheduledTaskExecution::class)->orderBy('created_at', 'desc');
}
public function server()
{
if ($this->application) {
if ($this->application->destination && $this->application->destination->server) {
$server = $this->application->destination->server;
return $server;
}
} elseif ($this->service) {
if ($this->service->destination && $this->service->destination->server) {
$server = $this->service->destination->server;
return $server;
}
} elseif ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
return $server;
}
}
return null;
}
}

View File

@@ -112,6 +112,16 @@ class Server extends BaseModel
'proxy',
];
protected $fillable = [
'name',
'ip',
'port',
'user',
'description',
'private_key_id',
'team_id',
];
protected $guarded = [];
public static function isReachable()
@@ -678,7 +688,7 @@ $schema://$host {
}
}
public function getDiskUsage()
public function getDiskUsage(): ?string
{
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
@@ -909,7 +919,7 @@ $schema://$host {
public function muxFilename()
{
return "{$this->ip}_{$this->port}_{$this->user}";
return $this->uuid;
}
public function team()

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
@@ -10,10 +11,10 @@ use OpenApi\Attributes as OA;
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'cleanup_after_percentage' => ['type' => 'integer'],
'concurrent_builds' => ['type' => 'integer'],
'dynamic_timeout' => ['type' => 'integer'],
'force_disabled' => ['type' => 'boolean'],
'force_server_cleanup' => ['type' => 'boolean'],
'is_build_server' => ['type' => 'boolean'],
'is_cloudflare_tunnel' => ['type' => 'boolean'],
'is_jump_server' => ['type' => 'boolean'],
@@ -37,6 +38,8 @@ use OpenApi\Attributes as OA;
'metrics_history_days' => ['type' => 'integer'],
'metrics_refresh_rate_seconds' => ['type' => 'integer'],
'metrics_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'],
'wildcard_domain' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
@@ -47,8 +50,25 @@ class ServerSetting extends Model
{
protected $guarded = [];
protected $casts = [
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
];
public function server()
{
return $this->belongsTo(Server::class);
}
public function dockerCleanupFrequency(): Attribute
{
return Attribute::make(
set: function ($value) {
return translate_cron_expression($value);
},
get: function ($value) {
return translate_cron_expression($value);
}
);
}
}

View File

@@ -7,9 +7,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Service model',
@@ -23,7 +24,7 @@ use Symfony\Component\Yaml\Yaml;
'description' => ['type' => 'string', 'description' => 'The description of the service.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The raw docker-compose.yml file of the service.'],
'docker_compose' => ['type' => 'string', 'description' => 'The docker-compose.yml file that is parsed and modified by Coolify.'],
'destination_type' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'],
'destination_type' => ['type' => 'string', 'description' => 'Destination type.'],
'destination_id' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label escape.'],
@@ -39,10 +40,20 @@ class Service extends BaseModel
{
use HasFactory, SoftDeletes;
private static $parserVersion = '3';
protected $guarded = [];
protected $appends = ['server_status'];
protected static function booted()
{
static::created(function ($service) {
$service->compose_parsing_version = self::$parserVersion;
$service->save();
});
}
public function isConfigurationChanged(bool $save = false)
{
$domains = $this->applications()->get()->pluck('fqdn')->sort()->toArray();
@@ -665,6 +676,32 @@ class Service extends BaseModel
$fields->put('GitLab', $data->toArray());
break;
case str($image)->contains('code-server'):
$data = collect([]);
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first();
if ($password) {
$data = $data->merge([
'Password' => [
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$sudoPassword = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_SUDOCODESERVER')->first();
if ($sudoPassword) {
$data = $data->merge([
'Sudo Password' => [
'key' => data_get($sudoPassword, 'key'),
'value' => data_get($sudoPassword, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Code Server', $data->toArray());
break;
}
}
$databases = $this->databases()->get();
@@ -711,8 +748,8 @@ class Service extends BaseModel
$fields->put('PostgreSQL', $data->toArray());
break;
case str($image)->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
@@ -761,10 +798,10 @@ class Service extends BaseModel
$fields->put('MySQL', $data->toArray());
break;
case str($image)->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA', 'MYSQL_DATABASE'];
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
@@ -811,6 +848,7 @@ class Service extends BaseModel
}
$fields->put('MariaDB', $data->toArray());
break;
}
}
@@ -945,7 +983,8 @@ class Service extends BaseModel
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("key LIKE 'SERVICE%' DESC, value ASC");
}
public function environment_variables_preview(): HasMany
@@ -961,21 +1000,36 @@ class Service extends BaseModel
public function saveComposeConfigs()
{
$workdir = $this->workdir();
$commands[] = "mkdir -p $workdir";
instant_remote_process([
"mkdir -p $workdir",
"cd $workdir",
], $this->server);
$filename = new Cuid2.'-docker-compose.yml';
Storage::disk('local')->put("tmp/{$filename}", $this->docker_compose);
$path = Storage::path("tmp/{$filename}");
instant_scp($path, "{$workdir}/docker-compose.yml", $this->server);
Storage::disk('local')->delete("tmp/{$filename}");
$commands[] = "cd $workdir";
$json = Yaml::parse($this->docker_compose);
$this->docker_compose = Yaml::dump($json, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$docker_compose_base64 = base64_encode($this->docker_compose);
$commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null";
$commands[] = 'rm -f .env || true';
$envs_from_coolify = $this->environment_variables()->get();
foreach ($envs_from_coolify as $env) {
$sorted = $envs_from_coolify->sortBy(function ($env) {
if (str($env->key)->startsWith('SERVICE_')) {
return 1;
}
if (str($env->value)->startsWith('$SERVICE_') || str($env->value)->startsWith('${SERVICE_')) {
return 2;
}
return 3;
});
foreach ($sorted as $env) {
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
}
if ($envs_from_coolify->count() === 0) {
if ($sorted->count() === 0) {
$commands[] = 'touch .env';
}
instant_remote_process($commands, $this->server);
@@ -983,7 +1037,14 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
if ($this->compose_parsing_version === '3') {
return newParser($this);
} elseif ($this->docker_compose_raw) {
return parseDockerComposeFile($this, $isNew);
} else {
return collect([]);
}
}
public function networks()

View File

@@ -44,7 +44,7 @@ class DockerCleanup extends Notification implements ShouldQueue
// $mail->view('emails.high-disk-usage', [
// 'name' => $this->server->name,
// 'disk_usage' => $this->disk_usage,
// 'threshold' => $this->cleanup_after_percentage,
// 'threshold' => $this->docker_cleanup_threshold,
// ]);
// return $mail;
// }

View File

@@ -17,7 +17,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
public $tries = 1;
public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage) {}
public function __construct(public Server $server, public int $disk_usage, public int $docker_cleanup_threshold) {}
public function via(object $notifiable): array
{
@@ -46,7 +46,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
$mail->view('emails.high-disk-usage', [
'name' => $this->server->name,
'disk_usage' => $this->disk_usage,
'threshold' => $this->cleanup_after_percentage,
'threshold' => $this->docker_cleanup_threshold,
]);
return $mail;
@@ -54,7 +54,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.";
$message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.";
return $message;
}
@@ -62,7 +62,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
public function toTelegram(): array
{
return [
'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Laravel\Telescope\TelescopeApplicationServiceProvider;
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// Telescope::night();
$this->hideSensitiveRequestDetails();
$isLocal = $this->app->environment('local');
Telescope::filter(function (IncomingEntry $entry) use ($isLocal) {
return $isLocal ||
$entry->isReportableException() ||
$entry->isFailedRequest() ||
$entry->isFailedJob() ||
$entry->isScheduledTask() ||
$entry->hasMonitoredTag();
});
}
/**
* Prevent sensitive request details from being logged by Telescope.
*/
protected function hideSensitiveRequestDetails(): void
{
if ($this->app->environment('local')) {
return;
}
Telescope::hideRequestParameters(['_token']);
Telescope::hideRequestHeaders([
'cookie',
'x-csrf-token',
'x-xsrf-token',
]);
}
/**
* Register the Telescope gate.
*
* This gate determines who can access Telescope in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewTelescope', function ($user) {
$root_user = User::find(0);
return in_array($user->email, [
$root_user->email,
]);
});
}
}

View File

@@ -91,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}");
ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green();
if ($deployments->count() > $concurrent_builds) {
return false;

View File

@@ -46,6 +46,7 @@ const SUPPORTED_OS = [
'centos fedora rhel ol rocky amzn almalinux',
'sles opensuse-leap opensuse-tumbleweed',
'arch',
'alpine',
];
const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment'];

View File

@@ -19,7 +19,7 @@ function generate_database_name(string $type): string
return $type.'-database-'.$cuid;
}
function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null): StandalonePostgresql
function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql
{
$destination = StandaloneDocker::where('uuid', $destinationUuid)->first();
if (! $destination) {
@@ -27,6 +27,7 @@ function create_standalone_postgresql($environmentId, $destinationUuid, ?array $
}
$database = new StandalonePostgresql;
$database->name = generate_database_name('postgresql');
$database->image = $databaseImage;
$database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environmentId;
$database->destination_id = $destination->id;

View File

@@ -140,6 +140,8 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
function generateApplicationContainerName(Application $application, $pull_request_id = 0)
{
// TODO: refactor generateApplicationContainerName, we do not need $application and $pull_request_id
$consistent_container_name = $application->settings->is_consistent_container_name_enabled;
$now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) {
@@ -251,7 +253,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
return $payload;
}
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both')
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null)
{
$labels = collect([]);
if ($serviceLabels) {
@@ -270,6 +272,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if (is_null($port) && ! is_null($onlyPort)) {
$port = $onlyPort;
}
if (is_null($port) && $predefinedPort) {
$port = $predefinedPort;
}
$labels->push("caddy_{$loop}={$schema}://{$host}");
$labels->push("caddy_{$loop}.header=-Server");
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
@@ -677,18 +682,19 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--sysctl',
'--ulimit',
'--device',
'--shm-size',
]);
$mapping = collect([
'--cap-add' => 'cap_add',
'--cap-drop' => 'cap_drop',
'--security-opt' => 'security_opt',
'--sysctl' => 'sysctls',
'--ulimit' => 'ulimits',
'--device' => 'devices',
'--init' => 'init',
'--ulimit' => 'ulimits',
'--privileged' => 'privileged',
'--ip' => 'ip',
'--shm-size' => 'shm_size',
]);
foreach ($matches as $match) {
$option = $match[1];
@@ -704,6 +710,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
$options = collect($options);
// Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js
foreach ($options as $option => $value) {
// ray($option,$value);
if (! data_get($mapping, $option)) {
continue;
}
@@ -728,6 +735,10 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
}
});
$compose_options->put($mapping[$option], $ulimits);
} elseif ($option === '--shm-size') {
if (! is_null($value) && is_array($value) && count($value) > 0) {
$compose_options->put($mapping[$option], $value[0]);
}
} else {
if ($list_options->contains($option)) {
if ($compose_options->has($mapping[$option])) {
@@ -749,6 +760,26 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
return $compose_options->toArray();
}
function generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $network)
{
$ipv4 = data_get($docker_run_options, 'ip.0');
$ipv6 = data_get($docker_run_options, 'ip6.0');
data_forget($docker_run_options, 'ip');
data_forget($docker_run_options, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$container_name], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$container_name]['networks'][$network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$container_name]['networks'][$network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$container_name] = array_merge_recursive($docker_compose['services'][$container_name], $docker_run_options);
return $docker_compose;
}
function validateComposeFile(string $compose, int $server_id): string|Throwable
{
return 'OK';

View File

@@ -146,12 +146,11 @@ function generate_default_proxy_configuration(Server $server)
'coolify.managed=true',
];
$config = [
'version' => '3.8',
'networks' => $array_of_networks->toArray(),
'services' => [
'traefik' => [
'container_name' => 'coolify-proxy',
'image' => 'traefik:v2.11',
'image' => 'traefik:v3.1',
'restart' => RESTART_MODE,
'extra_hosts' => [
'host.docker.internal:host-gateway',

View File

@@ -95,8 +95,26 @@ function generateScpCommand(Server $server, string $source, string $dest)
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
// Check if multiplexing is enabled
$muxEnabled = config('constants.ssh.mux_enabled', true);
// ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
if ($muxEnabled) {
// Always use multiplexing when enabled
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
ensureMultiplexedConnection($server);
// ray('Using SSH Multiplexing')->green();
} else {
// ray('Not using SSH Multiplexing')->red();
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$scp_command .= "-i {$privateKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
@@ -145,9 +163,20 @@ function generateSshCommand(Server $server, string $command)
$ssh_command = "timeout $timeout ssh ";
if (config('coolify.mux_enabled') && config('coolify.is_windows_docker_desktop') == false) {
$ssh_command .= "-o ControlMaster=auto -o ControlPersist={$muxPersistTime} -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r ";
// Check if multiplexing is enabled
$muxEnabled = config('constants.ssh.mux_enabled', true);
// ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
if ($muxEnabled) {
// Always use multiplexing when enabled
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
ensureMultiplexedConnection($server);
// ray('Using SSH Multiplexing')->green();
} else {
// ray('Not using SSH Multiplexing')->red();
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
@@ -167,11 +196,100 @@ function generateSshCommand(Server $server, string $command)
.$command.PHP_EOL
.$delimiter;
// ray($ssh_command);
return $ssh_command;
}
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false)
function ensureMultiplexedConnection(Server $server)
{
static $ensuredConnections = [];
if (isset($ensuredConnections[$server->id])) {
if (! shouldResetMultiplexedConnection($server)) {
// ray('Using Existing Multiplexed Connection')->green();
return;
}
}
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
$process = Process::run($checkCommand);
if ($process->exitCode() === 0) {
// ray('Existing Multiplexed Connection is Valid')->green();
$ensuredConnections[$server->id] = [
'timestamp' => now(),
'muxSocket' => $muxSocket,
];
return;
}
// ray('Establishing New Multiplexed Connection')->orange();
$privateKeyLocation = savePrivateKeyToFs($server);
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "
."-i {$privateKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR '
."-p {$server->port} "
."{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
}
$ensuredConnections[$server->id] = [
'timestamp' => now(),
'muxSocket' => $muxSocket,
];
// ray('Established New Multiplexed Connection')->green();
}
function shouldResetMultiplexedConnection(Server $server)
{
static $ensuredConnections = [];
if (! isset($ensuredConnections[$server->id])) {
return true;
}
$lastEnsured = $ensuredConnections[$server->id]['timestamp'];
$muxPersistTime = config('constants.ssh.mux_persist_time');
$resetInterval = strtotime($muxPersistTime) - time();
return $lastEnsured->addSeconds($resetInterval)->isPast();
}
function resetMultiplexedConnection(Server $server)
{
static $ensuredConnections = [];
if (isset($ensuredConnections[$server->id])) {
$muxSocket = $ensuredConnections[$server->id]['muxSocket'];
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
Process::run($closeCommand);
unset($ensuredConnections[$server->id]);
}
}
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
{
static $processCount = 0;
$processCount++;
$timeout = config('constants.ssh.command_timeout');
if ($command instanceof Collection) {
$command = $command->toArray();
@@ -180,10 +298,18 @@ function instant_remote_process(Collection|array $command, Server $server, bool
$command = parseCommandsByLineForSudo(collect($command), $server);
}
$command_string = implode("\n", $command);
$ssh_command = generateSshCommand($server, $command_string, $no_sudo);
$process = Process::timeout($timeout)->run($ssh_command);
$start_time = microtime(true);
$sshCommand = generateSshCommand($server, $command_string);
$process = Process::timeout($timeout)->run($sshCommand);
$end_time = microtime(true);
$execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds
// ray('SSH command execution time:', $execution_time.' ms')->orange();
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
if (! $throwError) {
return null;
@@ -223,7 +349,6 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
if (is_null($application_deployment_queue)) {
return collect([]);
}
// ray(data_get($application_deployment_queue, 'logs'));
try {
$decoded = json_decode(
data_get($application_deployment_queue, 'logs'),
@@ -233,7 +358,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
} catch (\JsonException $exception) {
return collect([]);
}
// ray($decoded );
$seenCommands = collect();
$formatted = collect($decoded);
if (! $is_debug_enabled) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
@@ -244,8 +369,43 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
return $i;
})
->reduce(function ($deploymentLogLines, $logItem) use ($seenCommands) {
$command = data_get($logItem, 'command');
$isStderr = data_get($logItem, 'type') === 'stderr';
$isNewCommand = ! is_null($command) && ! $seenCommands->first(function ($seenCommand) use ($logItem) {
return data_get($seenCommand, 'command') === data_get($logItem, 'command') && data_get($seenCommand, 'batch') === data_get($logItem, 'batch');
});
if ($isNewCommand) {
$deploymentLogLines->push([
'line' => $command,
'timestamp' => data_get($logItem, 'timestamp'),
'stderr' => $isStderr,
'hidden' => data_get($logItem, 'hidden'),
'command' => true,
]);
$seenCommands->push([
'command' => $command,
'batch' => data_get($logItem, 'batch'),
]);
}
$lines = explode(PHP_EOL, data_get($logItem, 'output'));
foreach ($lines as $line) {
$deploymentLogLines->push([
'line' => $line,
'timestamp' => data_get($logItem, 'timestamp'),
'stderr' => $isStderr,
'hidden' => data_get($logItem, 'hidden'),
]);
}
return $deploymentLogLines;
}, collect());
return $formatted;
}
function remove_iip($text)
@@ -258,6 +418,10 @@ function remove_mux_and_private_key(Server $server)
{
$muxFilename = $server->muxFilename();
$privateKeyLocation = savePrivateKeyToFs($server);
$closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
Process::run($closeCommand);
Storage::disk('ssh-mux')->delete($muxFilename);
Storage::disk('ssh-keys')->delete($privateKeyLocation);
}
@@ -267,7 +431,10 @@ function refresh_server_connection(?PrivateKey $private_key = null)
return;
}
foreach ($private_key->servers as $server) {
Storage::disk('ssh-mux')->delete($server->muxFilename());
$muxFilename = $server->muxFilename();
$closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
Process::run($closeCommand);
Storage::disk('ssh-mux')->delete($muxFilename);
}
}
@@ -277,24 +444,17 @@ function checkRequiredCommands(Server $server)
foreach ($commands as $command) {
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
if ($commandFound) {
ray($command.' found');
continue;
}
try {
instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server);
} catch (\Throwable $e) {
ray('could not install '.$command);
ray($e);
break;
}
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
if ($commandFound) {
ray($command.' found');
continue;
}
ray('could not install '.$command);
break;
}
}

View File

@@ -4,6 +4,7 @@ use App\Models\Application;
use App\Models\EnvironmentVariable;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Stringable;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
@@ -15,9 +16,9 @@ function collectRegex(string $name)
{
return "/{$name}\w+/";
}
function replaceVariables($variable)
function replaceVariables(string $variable): Stringable
{
return $variable->before('}')->replaceFirst('$', '')->replaceFirst('{', '');
return str($variable)->before('}')->replaceFirst('$', '')->replaceFirst('{', '');
}
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false)
@@ -53,7 +54,9 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
if ($isFile == 'OK') {
// If its a file & exists
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
if ($fileVolume->is_based_on_git) {
$fileVolume->content = $filesystemContent;
}
$fileVolume->is_directory = false;
$fileVolume->save();
} elseif ($isDir == 'OK') {

File diff suppressed because it is too large Load Diff

View File

@@ -14,10 +14,11 @@
"guzzlehttp/guzzle": "^7.5.0",
"laravel/fortify": "^v1.16.0",
"laravel/framework": "^v11",
"laravel/horizon": "^5.23.1",
"laravel/horizon": "^5.27.1",
"laravel/prompts": "^0.1.6",
"laravel/sanctum": "^v4.0",
"laravel/socialite": "^v5.14.0",
"laravel/telescope": "^5.2",
"laravel/tinker": "^v2.8.1",
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
@@ -93,7 +94,9 @@
},
"extra": {
"laravel": {
"dont-discover": []
"dont-discover": [
"laravel/telescope"
]
}
},
"config": {

854
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -199,6 +199,7 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TelescopeServiceProvider::class,
],

View File

@@ -6,7 +6,8 @@ return [
'contact' => 'https://coolify.io/docs/contact',
],
'ssh' => [
'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1m'),
'mux_enabled' => env('SSH_MUX_ENABLED', true),
'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1h'),
'connection_timeout' => 10,
'server_interval' => 20,
'command_timeout' => 7200,

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