Merge branch 'next' into git-cliff

This commit is contained in:
🏔️ Peak
2025-01-22 21:31:12 +01:00
committed by GitHub
50 changed files with 722 additions and 236 deletions

View File

@@ -13,16 +13,16 @@ jobs:
id: stale
with:
stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.'
stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.'
stale-pr-message: 'This pull request requires attention. If no changes or response is received within the next few days, it will be automatically closed. Please update your PR or leave a comment with the requested information.'
close-issue-message: 'This issue has been automatically closed due to inactivity.'
close-pr-message: 'This pull request has been automatically closed due to inactivity.'
close-pr-message: 'Thank you for your contribution. Due to inactivity, this PR was automatically closed. If you would like to continue working on this change in the future, feel free to reopen this PR or submit a new one.'
days-before-stale: 14
days-before-close: 7
stale-issue-label: '⏱︎ Stale'
stale-pr-label: '⏱︎ Stale'
only-labels: '💤 Waiting for feedback'
only-labels: '💤 Waiting for feedback, 💤 Waiting for changes'
remove-stale-when-updated: true
operations-per-run: 100
labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback'
labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback, 💤 Waiting for changes'
close-issue-reason: 'not_planned'
exempt-all-milestones: false

View File

@@ -19,8 +19,12 @@ jobs:
script: |
const { owner, repo } = context.repo;
async function processIssue(issueNumber) {
async function processIssue(issueNumber, isFromPR = false, prBaseBranch = null) {
try {
if (isFromPR && prBaseBranch !== 'main') {
return;
}
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
@@ -59,19 +63,19 @@ jobs:
}
}
if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const issue = context.payload.issue || context.payload.pull_request;
await processIssue(issue.number);
if (context.eventName === 'issues') {
await processIssue(context.payload.issue.number);
}
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const pr = context.payload.pull_request;
if (pr.body) {
await processIssue(pr.number);
if (pr.merged && pr.base.ref === 'main' && pr.body) {
const issueReferences = pr.body.match(/#(\d+)/g);
if (issueReferences) {
for (const reference of issueReferences) {
const issueNumber = parseInt(reference.substring(1));
await processIssue(issueNumber);
await processIssue(issueNumber, true, pr.base.ref);
}
}
}

View File

@@ -38,7 +38,7 @@ jobs:
- 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
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -77,7 +77,7 @@ jobs:
- 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
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -119,7 +119,7 @@ jobs:
- 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
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |

View File

@@ -38,7 +38,7 @@ jobs:
- 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
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -77,7 +77,7 @@ jobs:
- 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
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -119,7 +119,7 @@ jobs:
- 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
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |

View File

@@ -42,7 +42,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -82,7 +82,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -125,7 +125,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |

View File

@@ -42,7 +42,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -82,7 +82,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -125,7 +125,7 @@ jobs:
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |

View File

@@ -4,6 +4,8 @@
You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document.
## Table of Contents
1. [Setup Development Environment](#1-setup-development-environment)

29
TECH_STACK.md Normal file
View File

@@ -0,0 +1,29 @@
# Coolify Technology Stack
## Frontend
- Livewire and Alpine.js
- Blade (PHP templating engine)
- Tailwind CSS
- Monaco Editor (Code editor component)
- XTerm.js (Terminal component)
## Backend
- Laravel 11 (PHP Framework)
- PostgreSQL 15 (Database)
- Redis 7 (Caching & Real-time features)
- Soketi (WebSocket Server)
## DevOps & Infrastructure
- Docker & Docker Compose
- Nginx (Web Server)
- S6 Overlay (Process Supervisor)
- GitHub Actions (CI/CD)
## Languages
- PHP 8.4
- JavaScript
- Shell/Bash scripts

View File

@@ -1122,7 +1122,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
$nixpacks_php_fallback_path->value = '/index.php';
$nixpacks_php_fallback_path->is_build_time = false;
$nixpacks_php_fallback_path->application_id = $this->application->id;
$nixpacks_php_fallback_path->resourceable_id = $this->application->id;
$nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application';
$nixpacks_php_fallback_path->save();
}
if (! $nixpacks_php_root_dir) {
@@ -1130,7 +1131,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
$nixpacks_php_root_dir->value = '/app/public';
$nixpacks_php_root_dir->is_build_time = false;
$nixpacks_php_root_dir->application_id = $this->application->id;
$nixpacks_php_root_dir->resourceable_id = $this->application->id;
$nixpacks_php_root_dir->resourceable_type = 'App\Models\Application';
$nixpacks_php_root_dir->save();
}
@@ -2286,8 +2288,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function generate_build_env_variables()
{
$variables = collect($this->nixpacks_plan_json->get('variables'));
if ($this->application->build_pack === 'nixpacks') {
$variables = collect($this->nixpacks_plan_json->get('variables'));
} else {
$this->generate_env_variables();
$variables = collect([])->merge($this->env_args);
}
$this->build_args = $variables->map(function ($value, $key) {
$value = escapeshellarg($value);
return "--build-arg {$key}={$value}";
});
}

View File

@@ -25,7 +25,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
if (isDev() || isCloud()) {
if (isDev()) {
return;
}
$response = Http::retry(3, 1000)->get(config('constants.services.official'));

View File

@@ -442,6 +442,7 @@ class General extends Component
{
$config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json');
dd($config);
return response()->streamDownload(function () use ($config) {
echo $config;

View File

@@ -88,12 +88,12 @@ class General extends Component
if (version_compare($this->redis_version, '6.0', '>=')) {
$this->database->runtime_environment_variables()->updateOrCreate(
['key' => 'REDIS_USERNAME'],
['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id]
['value' => $this->redis_username, 'resourceable_id' => $this->database->id]
);
}
$this->database->runtime_environment_variables()->updateOrCreate(
['key' => 'REDIS_PASSWORD'],
['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id]
['value' => $this->redis_password, 'resourceable_id' => $this->database->id]
);
$this->database->save();

View File

@@ -77,18 +77,28 @@ class Show extends Component
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
if ($this->isSharedVariable) {
$this->validate([
'key' => 'required|string',
'value' => 'nullable',
'is_multiline' => 'required|boolean',
'is_literal' => 'required|boolean',
'is_shown_once' => 'required|boolean',
'real_value' => 'nullable',
]);
} else {
$this->validate();
$this->env->is_build_time = $this->is_build_time;
$this->env->is_required = $this->is_required;
$this->env->is_shared = $this->is_shared;
}
$this->env->key = $this->key;
$this->env->value = $this->value;
$this->env->is_build_time = $this->is_build_time;
$this->env->is_multiline = $this->is_multiline;
$this->env->is_literal = $this->is_literal;
$this->env->is_shown_once = $this->is_shown_once;
$this->env->is_required = $this->is_required;
$this->env->is_shared = $this->is_shared;
$this->env->save();
} else {
$this->key = $this->env->key;
$this->value = $this->env->value;
$this->is_build_time = $this->env->is_build_time ?? false;
@@ -141,30 +151,15 @@ class Show extends Component
public function submit()
{
try {
if ($this->isSharedVariable) {
$this->validate([
'key' => 'required|string',
'value' => 'nullable',
'is_shown_once' => 'required|boolean',
]);
} else {
$this->validate();
}
if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) {
$oldValue = $this->env->getOriginal('value');
$this->value = $oldValue;
$this->dispatch('error', 'Required environment variable cannot be empty.');
$this->dispatch('error', 'Required environment variables cannot be empty.');
return;
}
$this->serialize();
if ($this->isSharedVariable) {
unset($this->is_required);
}
$this->syncData(true);
$this->dispatch('success', 'Environment variable updated.');
$this->dispatch('envsUpdated');

View File

@@ -27,6 +27,8 @@ class ExecuteContainerCommand extends Component
public Collection $servers;
public bool $hasShell = true;
protected $rules = [
'server' => 'required',
'container' => 'required',
@@ -141,6 +143,16 @@ class ExecuteContainerCommand extends Component
}
}
private function checkShellAvailability(Server $server, string $container): bool
{
$escapedContainer = escapeshellarg($container);
$result = instant_remote_process([
"docker exec {$escapedContainer} which bash || docker exec {$escapedContainer} which sh",
], $server, false);
return ! empty($result);
}
#[On('connectToServer')]
public function connectToServer()
{
@@ -148,6 +160,7 @@ class ExecuteContainerCommand extends Component
if ($this->server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
$this->hasShell = true;
$this->dispatch(
'send-terminal-command',
false,
@@ -201,6 +214,11 @@ class ExecuteContainerCommand extends Component
throw new \RuntimeException('Server ownership verification failed.');
}
$this->hasShell = $this->checkShellAvailability($server, data_get($container, 'container.Names'));
if (! $this->hasShell) {
return;
}
$this->dispatch(
'send-terminal-command',
true,

View File

@@ -3,6 +3,8 @@
namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use App\Services\ConfigurationGenerator;
use App\Traits\HasConfiguration;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -105,7 +107,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
use HasFactory, SoftDeletes;
use HasConfiguration, HasFactory, SoftDeletes;
private static $parserVersion = '4';
@@ -1640,35 +1642,28 @@ class Application extends BaseModel
}
}
public function getLimits(): array
{
return [
'limits_memory' => $this->limits_memory,
'limits_memory_swap' => $this->limits_memory_swap,
'limits_memory_swappiness' => $this->limits_memory_swappiness,
'limits_memory_reservation' => $this->limits_memory_reservation,
'limits_cpus' => $this->limits_cpus,
'limits_cpuset' => $this->limits_cpuset,
'limits_cpu_shares' => $this->limits_cpu_shares,
];
}
public function generateConfig($is_json = false)
{
$config = collect([]);
if ($this->build_pack = 'nixpacks') {
$config = collect([
'build_pack' => 'nixpacks',
'docker_registry_image_name' => $this->docker_registry_image_name,
'docker_registry_image_tag' => $this->docker_registry_image_tag,
'install_command' => $this->install_command,
'build_command' => $this->build_command,
'start_command' => $this->start_command,
'base_directory' => $this->base_directory,
'publish_directory' => $this->publish_directory,
'custom_docker_run_options' => $this->custom_docker_run_options,
'ports_exposes' => $this->ports_exposes,
'ports_mappings' => $this->ports_mapping,
'settings' => collect([
'is_static' => $this->settings->is_static,
]),
]);
}
$config = $config->filter(function ($value) {
return str($value)->isNotEmpty();
});
$generator = new ConfigurationGenerator($this);
if ($is_json) {
return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
return $generator->toJson();
}
return $config;
return $generator->toArray();
}
public function setConfig($config)

View File

@@ -4,9 +4,7 @@ namespace App\Models;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Environment Variable model',
@@ -30,7 +28,7 @@ use Visus\Cuid2\Cuid2;
'updated_at' => ['type' => 'string'],
]
)]
class EnvironmentVariable extends Model
class EnvironmentVariable extends BaseModel
{
protected $guarded = [];
@@ -49,12 +47,6 @@ class EnvironmentVariable extends Model
protected static function booted()
{
static::creating(function (Model $model) {
if (! $model->uuid) {
$model->uuid = (string) new Cuid2;
}
});
static::created(function (EnvironmentVariable $environment_variable) {
if ($environment_variable->resourceable_type === Application::class && ! $environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)

View File

@@ -53,6 +53,7 @@ class EmailChannel
if (! $type) {
throw new Exception('No email settings found.');
}
config()->set('mail.default', $type);
return;
}

View File

@@ -0,0 +1,194 @@
<?php
namespace App\Services;
use App\Models\Application;
use Symfony\Component\Yaml\Yaml;
class ConfigurationGenerator
{
protected array $config = [];
public function __construct(protected Application $resource)
{
$this->generateConfig();
}
protected function generateConfig(): void
{
if ($this->resource instanceof Application) {
$this->config = [
'id' => $this->resource->id,
'name' => $this->resource->name,
'uuid' => $this->resource->uuid,
'description' => $this->resource->description,
'coolify_details' => [
'project_uuid' => $this->resource->project()->uuid,
'environment_uuid' => $this->resource->environment->uuid,
'destination_type' => $this->resource->destination_type,
'destination_id' => $this->resource->destination_id,
'source_type' => $this->resource->source_type,
'source_id' => $this->resource->source_id,
'private_key_id' => $this->resource->private_key_id,
],
'post_deployment_command' => $this->resource->post_deployment_command,
'post_deployment_command_container' => $this->resource->post_deployment_command_container,
'pre_deployment_command' => $this->resource->pre_deployment_command,
'pre_deployment_command_container' => $this->resource->pre_deployment_command_container,
'build' => [
'type' => $this->resource->build_pack,
'static_image' => $this->resource->static_image,
'base_directory' => $this->resource->base_directory,
'publish_directory' => $this->resource->publish_directory,
'dockerfile' => $this->resource->dockerfile,
'dockerfile_location' => $this->resource->dockerfile_location,
'dockerfile_target_build' => $this->resource->dockerfile_target_build,
'custom_docker_run_options' => $this->resource->custom_docker_options,
'compose_parsing_version' => $this->resource->compose_parsing_version,
'docker_compose' => $this->resource->docker_compose,
'docker_compose_location' => $this->resource->docker_compose_location,
'docker_compose_raw' => $this->resource->docker_compose_raw,
'docker_compose_domains' => $this->resource->docker_compose_domains,
'docker_compose_custom_start_command' => $this->resource->docker_compose_custom_start_command,
'docker_compose_custom_build_command' => $this->resource->docker_compose_custom_build_command,
'install_command' => $this->resource->install_command,
'build_command' => $this->resource->build_command,
'start_command' => $this->resource->start_command,
'watch_paths' => $this->resource->watch_paths,
],
'source' => [
'git_repository' => $this->resource->git_repository,
'git_branch' => $this->resource->git_branch,
'git_commit_sha' => $this->resource->git_commit_sha,
'repository_project_id' => $this->resource->repository_project_id,
],
'docker_registry_image' => $this->getDockerRegistryImage(),
'domains' => [
'fqdn' => $this->resource->fqdn,
'ports_exposes' => $this->resource->ports_exposes,
'ports_mappings' => $this->resource->ports_mappings,
'redirect' => $this->resource->redirect,
'custom_nginx_configuration' => $this->resource->custom_nginx_configuration,
],
'environment_variables' => [
'production' => $this->getEnvironmentVariables(),
'preview' => $this->getPreviewEnvironmentVariables(),
],
'settings' => $this->getApplicationSettings(),
'preview' => $this->getPreview(),
'limits' => $this->resource->getLimits(),
'health_check' => [
'health_check_path' => $this->resource->health_check_path,
'health_check_port' => $this->resource->health_check_port,
'health_check_host' => $this->resource->health_check_host,
'health_check_method' => $this->resource->health_check_method,
'health_check_return_code' => $this->resource->health_check_return_code,
'health_check_scheme' => $this->resource->health_check_scheme,
'health_check_response_text' => $this->resource->health_check_response_text,
'health_check_interval' => $this->resource->health_check_interval,
'health_check_timeout' => $this->resource->health_check_timeout,
'health_check_retries' => $this->resource->health_check_retries,
'health_check_start_period' => $this->resource->health_check_start_period,
'health_check_enabled' => $this->resource->health_check_enabled,
],
'webhooks_secrets' => [
'manual_webhook_secret_github' => $this->resource->manual_webhook_secret_github,
'manual_webhook_secret_gitlab' => $this->resource->manual_webhook_secret_gitlab,
'manual_webhook_secret_bitbucket' => $this->resource->manual_webhook_secret_bitbucket,
'manual_webhook_secret_gitea' => $this->resource->manual_webhook_secret_gitea,
],
'swarm' => [
'swarm_replicas' => $this->resource->swarm_replicas,
'swarm_placement_constraints' => $this->resource->swarm_placement_constraints,
],
];
}
}
protected function getPreview(): array
{
return [
'preview_url_template' => $this->resource->preview_url_template,
];
}
protected function getDockerRegistryImage(): array
{
return [
'image' => $this->resource->docker_registry_image_name,
'tag' => $this->resource->docker_registry_image_tag,
];
}
protected function getEnvironmentVariables(): array
{
$variables = collect([]);
foreach ($this->resource->environment_variables as $env) {
$variables->push([
'key' => $env->key,
'value' => $env->value,
'is_build_time' => $env->is_build_time,
'is_preview' => $env->is_preview,
'is_multiline' => $env->is_multiline,
]);
}
return $variables->toArray();
}
protected function getPreviewEnvironmentVariables(): array
{
$variables = collect([]);
foreach ($this->resource->environment_variables_preview as $env) {
$variables->push([
'key' => $env->key,
'value' => $env->value,
'is_build_time' => $env->is_build_time,
'is_preview' => $env->is_preview,
'is_multiline' => $env->is_multiline,
]);
}
return $variables->toArray();
}
protected function getApplicationSettings(): array
{
$removedKeys = ['id', 'application_id', 'created_at', 'updated_at'];
$settings = $this->resource->settings->attributesToArray();
$settings = collect($settings)->filter(function ($value, $key) use ($removedKeys) {
return ! in_array($key, $removedKeys);
})->sortBy(function ($value, $key) {
return $key;
})->toArray();
return $settings;
}
public function saveJson(string $path): void
{
file_put_contents($path, json_encode($this->config, JSON_PRETTY_PRINT));
}
public function saveYaml(string $path): void
{
file_put_contents($path, Yaml::dump($this->config, 6, 2));
}
public function toArray(): array
{
return $this->config;
}
public function toJson(): string
{
return json_encode($this->config, JSON_PRETTY_PRINT);
}
public function toYaml(): string
{
return Yaml::dump($this->config, 6, 2);
}
}

View File

@@ -43,7 +43,11 @@ class DockerImageParser
public function getFullImageNameWithoutTag(): string
{
return $this->registryUrl.'/'.$this->imageName;
if ($this->registryUrl) {
return $this->registryUrl.'/'.$this->imageName;
}
return $this->imageName;
}
public function getRegistryUrl(): string

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Traits;
use App\Services\ConfigurationGenerator;
trait HasConfiguration
{
public function generateConfigurationFiles(): void
{
$generator = new ConfigurationGenerator($this);
$configDir = base_configuration_dir()."/{$this->uuid}";
if (! is_dir($configDir)) {
mkdir($configDir, 0755, true);
}
$generator->saveJson($configDir.'/coolify.json');
$generator->saveYaml($configDir.'/coolify.yaml');
// Generate a README file with basic information
file_put_contents(
$configDir.'/README.md',
generate_readme_file($this->name, now()->toIso8601String())
);
}
public function getConfigurationAsJson(): string
{
return (new ConfigurationGenerator($this))->toJson();
}
public function getConfigurationAsYaml(): string
{
return (new ConfigurationGenerator($this))->toYaml();
}
public function getConfigurationAsArray(): array
{
return (new ConfigurationGenerator($this))->toArray();
}
}

View File

@@ -0,0 +1,10 @@
<?php
// To prevent github actions from failing
function env()
{
return null;
}
$version = include 'config/constants.php';
echo $version['coolify']['helper_version'] ?: 'unknown';

View File

@@ -0,0 +1,10 @@
<?php
// To prevent github actions from failing
function env()
{
return null;
}
$version = include 'config/constants.php';
echo $version['coolify']['realtime_version'] ?: 'unknown';

View File

@@ -2645,7 +2645,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($value?->startsWith('$')) {
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'application_id' => $resource->id,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
])->first();
$value = replaceVariables($value);
@@ -2653,7 +2654,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($value->startsWith('SERVICE_')) {
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'application_id' => $resource->id,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if (! is_null($command)) {
@@ -2676,7 +2678,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'key' => $key,
'value' => $fqdn,
'is_build_time' => false,
'application_id' => $resource->id,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
]);
}
@@ -2687,7 +2690,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'key' => $key,
'value' => $generatedValue,
'is_build_time' => false,
'application_id' => $resource->id,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
]);
}
@@ -2712,7 +2716,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'application_id' => $resource->id,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
])->first();
if ($foundEnv) {
@@ -2722,7 +2727,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($foundEnv) {
$foundEnv->update([
'key' => $key,
'application_id' => $resource->id,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_build_time' => $isBuildTime,
'value' => $defaultValue,
]);
@@ -2731,7 +2737,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'key' => $key,
'value' => $defaultValue,
'is_build_time' => $isBuildTime,
'application_id' => $resource->id,
'resourceable_type' => get_class($resource),
'resourceable_id' => $resource->id,
'is_preview' => false,
]);
}
@@ -2872,7 +2879,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc');
data_set($service, 'environment', $serviceVariables->toArray());
updateCompose($service);
return $service;
});

View File

@@ -2,7 +2,9 @@
return [
'coolify' => [
'version' => '4.0.0-beta.383',
'version' => '4.0.0-beta.389',
'helper_version' => '1.0.5',
'realtime_version' => '1.0.5',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
try {
DB::table('application_deployment_queues')
->whereNull('finished_at')
->update(['finished_at' => DB::raw('updated_at')]);
} catch (\Exception $e) {
\Log::error('Failed to update not set finished_at timestamps for application_deployment_queues: '.$e->getMessage());
}
try {
DB::table('scheduled_database_backup_executions')
->whereNull('finished_at')
->update(['finished_at' => DB::raw('updated_at')]);
} catch (\Exception $e) {
\Log::error('Failed to update not set finished_at timestamps for scheduled_database_backup_executions: '.$e->getMessage());
}
try {
DB::table('scheduled_task_executions')
->whereNull('finished_at')
->update(['finished_at' => DB::raw('updated_at')]);
} catch (\Exception $e) {
\Log::error('Failed to update not set finished_at timestamps for scheduled_task_executions: '.$e->getMessage());
}
}
};

View File

@@ -0,0 +1,19 @@
<?php
use App\Models\EnvironmentVariable;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Log;
return new class extends Migration
{
public function up(): void
{
try {
EnvironmentVariable::whereNull('resourceable_id')->each(function (EnvironmentVariable $environmentVariable) {
$environmentVariable->delete();
});
} catch (\Exception $e) {
Log::error('Failed to delete wrongly created environment variables: '.$e->getMessage());
}
}
};

View File

@@ -1,5 +1,6 @@
# Versions
# https://hub.docker.com/_/alpine
ARG BASE_IMAGE=alpine:3.21
# https://download.docker.com/linux/static/stable/
@@ -11,7 +12,7 @@ ARG DOCKER_BUILDX_VERSION=0.19.3
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.36.2
# https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.30.0
ARG NIXPACKS_VERSION=1.32.0
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2024-11-21T17-21-54Z

View File

@@ -119,6 +119,7 @@ COPY --chown=www-data:www-data storage ./storage
COPY --chown=www-data:www-data templates ./templates
COPY --chown=www-data:www-data resources/views ./resources/views
COPY --chown=www-data:www-data artisan artisan
COPY --chown=www-data:www-data openapi.yaml ./openapi.yaml
RUN composer dump-autoload

8
package-lock.json generated
View File

@@ -22,7 +22,7 @@
"pusher-js": "8.4.0-rc2",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "3.4.17",
"vite": "6.0.7",
"vite": "6.0.11",
"vue": "3.5.13"
}
},
@@ -2784,9 +2784,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
"integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==",
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -16,7 +16,7 @@
"pusher-js": "8.4.0-rc2",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "3.4.17",
"vite": "6.0.7",
"vite": "6.0.11",
"vue": "3.5.13"
},
"dependencies": {

21
public/svgs/flipt.svg Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="logosandtypes_com" data-name="logosandtypes com" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150">
<defs>
<style>
.cls-1 {
fill: #7c3aed;
fill-rule: evenodd;
}
.cls-2 {
fill: none;
}
</style>
</defs>
<g id="Layer_3" data-name="Layer 3">
<g id="Layer_2" data-name="Layer 2">
<path id="Layer_3-2" data-name="Layer 3-2" class="cls-2" d="M0,0H150V150H0V0Z"/>
</g>
</g>
<path class="cls-1" d="M140.19,76.45c-3.24-14.68-11.77-43.35-15.1-56.52-6.68,.83-18.4,2.17-24.91,2.97,0,0-11.73,1.41-11.73,1.41-1.91,.27-3.79-.53-4.19-2.11-.33-1.03-1.18-5.12-1.95-5.88-.9-.94-2.35-.97-3.55-.69-15.06,1.87-49.7,6.21-49.7,6.21l-3.47-12.55-15.25,1.87L45.82,141.9l14.93-1.99L34.32,41.31s33.41-4.16,48.14-6.1c3.63-.62,5.03,.57,7.18,4.1,.84,1.41,1.68,1.76,3.47,1.52,.07,0,20.89-2.41,20.96-2.38,1.01,3.73,3.08,11.11,4.07,14.81l-18.09,2.35c-2.75,.35-4.55-.47-6.1-2.7-.47-.68-1.17-2.1-1.89-2.5-.7-.52-1.48-.57-2.66-.43-9.03,1.12-35.96,4.46-35.96,4.46l3.23,11.84s24.42-2.93,33.09-4.1c3.05-.61,5.65,.3,6.98,4.42,.31,.96,1.37,1.47,2.38,1.54l22.33-2.92,4.14,14.7s-18.11,2.66-18.11,2.66c-2.87,.35-4.09,.2-5.64-2.03-.17-.28-1.1-1.45-1.63-1.98-1.28-1.28-3.09-1.9-4.88-1.66l-34.58,4.5,4.55,16.65s25.11-3.45,35.45-4.45c3.31-.44,2.75,2.76,3.9,4.64,.61,.94,1.73,1.56,4.12,1.22l14.75-1.85c5.81-.71,11.8-1.61,15.16-6.4,1.51-2.16,2.52-6.64,2.34-9.02-.09-1.22-.56-4.54-.82-5.74Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -22,7 +22,7 @@
<a wire:navigate class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}"
href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}">Log
Drains</a>
<a wire:navigate class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}"
<a class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}"
href="{{ route('server.charts', ['server_uuid' => $server->uuid]) }}">Metrics</a>
@endif
@if (!$server->isLocalhost())

View File

@@ -6,12 +6,8 @@
])
<div class="flex items-center">
<div class="flex items-center">
<span wire:loading.delay.longer>
<div class="badge badge-warning"></div>
</span>
<span wire:loading.remove.delay.longer>
<div class="badge badge-success"></div>
</span>
<div wire:loading.delay.longer wire:target="checkProxy(true)" class="badge badge-warning"></div>
<div wire:loading.remove.delay.longer wire:target="checkProxy(true)" class="badge badge-success"></div>
<div class="pl-2 pr-1 text-xs font-bold tracking-wider text-success"
@if ($title) title="{{ $title }}" @endif>
@if ($lastDeploymentLink)

View File

@@ -74,8 +74,7 @@
href="{{ route('project.application.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
wire:navigate>Resource Operations</a>
<a class="menu-item" wire:current.exact="menu-item-active"
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
wire:navigate>Metrics</a>
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}" >Metrics</a>
<a class="menu-item" wire:current.exact="menu-item-active"
href="{{ route('project.application.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"
wire:navigate>Tags</a>

View File

@@ -5,13 +5,14 @@
<x-forms.button type="submit">
Save
</x-forms.button>
{{--
<x-forms.button wire:click="downloadConfig">
{{-- <x-forms.button wire:click="downloadConfig">
Download Config
<x-modal-input buttonTitle="Upload Config" title="Upload Config" :closeOutside="false">
</x-forms.button> --}}
{{-- <x-modal-input buttonTitle="Upload Config" title="Upload Config" :closeOutside="false">
<livewire:project.shared.upload-config :applicationId="$application->id" />
</x-modal-input>
--}}
</x-modal-input> --}}
</div>
<div>General configuration for your application.</div>
<div class="flex flex-col gap-2 py-4">

View File

@@ -20,8 +20,8 @@
href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"
wire:navigate>Persistent Storage</a>
<a class='menu-item' wire:current.exact="menu-item-active"
href="{{ route('project.database.import-backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"
wire:navigate>Import Backups</a>
href="{{ route('project.database.import-backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Import
Backups</a>
<a class='menu-item' wire:current.exact="menu-item-active"
href="{{ route('project.database.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"
wire:navigate>Webhooks</a>
@@ -32,8 +32,7 @@
href="{{ route('project.database.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"
wire:navigate>Resource Operations</a>
<a class='menu-item' wire:current.exact="menu-item-active"
href="{{ route('project.database.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"
wire:navigate>Metrics</a>
href="{{ route('project.database.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Metrics</a>
<a class='menu-item' wire:current.exact="menu-item-active"
href="{{ route('project.database.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"
wire:navigate>Tags</a>

View File

@@ -13,7 +13,7 @@
@if ($serviceDatabase?->isBackupSolutionAvailable())
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'"
wire:navigate href="#">Backups</a>
wire:navigate href="#backups">Backups</a>
@endif
</div>
<div class="w-full">

View File

@@ -55,10 +55,10 @@
<h3>Preview Deployments Environment Variables</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
{{-- @foreach ($resource->environment_variables_preview as $env)
@foreach ($resource->environment_variables_preview as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" :type="$resource->type()" />
@endforeach --}}
@endforeach
@endif
@else
<form wire:submit.prevent='submit' class="flex flex-col gap-2">

View File

@@ -16,43 +16,58 @@
@elseif ($type === 'server')
<x-server.navbar :server="$server" :parameters="$parameters" />
@endif
@if ($type === 'server')
<form class="w-full" wire:submit="$dispatchSelf('connectToServer')" wire:init="$dispatchSelf('connectToServer')">
<x-forms.button class="w-full" type="submit">Reconnect</x-forms.button>
</form>
<div class="mx-auto w-full">
<livewire:project.shared.terminal />
@if(!$hasShell)
<div class="flex items-center justify-center w-full py-4 mx-auto">
<div class="p-4 w-full rounded border dark:bg-coolgray-100 dark:border-coolgray-300">
<div class="flex flex-col items-center justify-center space-y-4">
<svg class="w-12 h-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div class="text-center">
<h3 class="text-lg font-medium">Terminal Not Available</h3>
<p class="mt-2 text-sm text-gray-500">No shell (bash/sh) is available in this container. Please ensure either bash or sh is installed to use the terminal.</p>
</div>
</div>
</div>
</div>
@else
@if (count($containers) > 0)
@if (count($containers) === 1)
<form class="w-full pt-4" wire:submit="$dispatchSelf('connectToContainer')"
wire:init="$dispatchSelf('connectToContainer')">
<x-forms.button class="w-full" type="submit">Reconnect</x-forms.button>
</form>
@else
<form class="w-full pt-4 flex gap-2 flex-col" wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select label="Container" id="container" required wire:model="selected_container">
@foreach ($containers as $container)
@if ($loop->first)
<option disabled value="default">Select a container</option>
@endif
<option value="{{ data_get($container, 'container.Names') }}">
{{ data_get($container, 'container.Names') }}
({{ data_get($container, 'server.name') }})
</option>
@endforeach
</x-forms.select>
<x-forms.button class="w-full" type="submit">
Connect
</x-forms.button>
</form>
@endif
@if ($type === 'server')
<form class="w-full" wire:submit="$dispatchSelf('connectToServer')" wire:init="$dispatchSelf('connectToServer')">
<x-forms.button class="w-full" type="submit">Reconnect</x-forms.button>
</form>
<div class="mx-auto w-full">
<livewire:project.shared.terminal />
</div>
@else
<div class="pt-4">No containers are running.</div>
@if (count($containers) === 0)
<div class="pt-4">No containers are running.</div>
@else
@if (count($containers) === 1)
<form class="w-full pt-4" wire:submit="$dispatchSelf('connectToContainer')"
wire:init="$dispatchSelf('connectToContainer')">
<x-forms.button class="w-full" type="submit">Reconnect</x-forms.button>
</form>
@else
<form class="w-full pt-4 flex gap-2 flex-col" wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select label="Container" id="container" required wire:model="selected_container">
@foreach ($containers as $container)
@if ($loop->first)
<option disabled value="default">Select a container</option>
@endif
<option value="{{ data_get($container, 'container.Names') }}">
{{ data_get($container, 'container.Names') }}
({{ data_get($container, 'server.name') }})
</option>
@endforeach
</x-forms.select>
<x-forms.button class="w-full" type="submit">Connect</x-forms.button>
</form>
@endif
<div class="mx-auto w-full">
<livewire:project.shared.terminal />
</div>
@endif
@endif
@endif
</div>

View File

@@ -8,7 +8,7 @@
@elseif(!$resource->destination->server->isMetricsEnabled())
<div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div>
<div> Go to <a class="underline dark:text-white"
href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to
wire:navigate href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to
enable
it.</div>
@else

View File

@@ -1,9 +1,4 @@
<div id="terminal-container" x-data="terminalData()">
{{-- <div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]">
<div class="p-1 w-full h-full rounded border dark:bg-coolgray-100 dark:border-coolgray-300">
<span class="font-mono text-sm text-gray-500" x-text="message"></span>
</div>
</div> --}}
<div x-ref="terminalWrapper"
:class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
<div id="terminal" wire:ignore></div>

View File

@@ -6,72 +6,75 @@
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="docker-cleanup" />
<div class="w-full">
<div>
<div class="flex items-center gap-2">
<h2>Docker Cleanup</h2>
<form wire:submit='submit'>
<div>
<div class="flex items-center gap-2">
<h2>Docker Cleanup</h2>
<x-forms.button type="submit">Save</x-forms.button>
</div>
<div class="mt-3 mb-4">Configure Docker cleanup settings for your server.</div>
</div>
<div class="mt-3 mb-4">Configure Docker cleanup settings for your server.</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex gap-4">
<h3>Docker Cleanup</h3>
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup"
isHighlightedButton submitAction="manualCleanup" :actions="[
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Permanently deletes all unused images',
'Clears build cache',
'Removes old versions of the Coolify helper image',
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
'Optionally permanently deletes all unused networks (if enabled in advanced options).',
]" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Trigger Docker Cleanup" />
</div>
<div class="flex flex-wrap items-center gap-4">
<x-forms.input placeholder="*/10 * * * *" id="dockerCleanupFrequency"
label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@if (!$forceDockerCleanup)
<x-forms.input id="dockerCleanupThreshold" label="Docker cleanup threshold (%)" required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
@endif
<div class="flex flex-col gap-2">
<div class="flex gap-4">
<h3>Docker Cleanup</h3>
<x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Manual Cleanup"
isHighlightedButton submitAction="manualCleanup" :actions="[
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Permanently deletes all unused images',
'Clears build cache',
'Removes old versions of the Coolify helper image',
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
'Optionally permanently deletes all unused networks (if enabled in advanced options).',
]" :confirmWithText="false"
:confirmWithPassword="false" step2ButtonText="Trigger Docker Cleanup" />
</div>
<div class="flex flex-wrap items-center gap-4">
<x-forms.input placeholder="*/10 * * * *" id="dockerCleanupFrequency"
label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@if (!$forceDockerCleanup)
<x-forms.input id="dockerCleanupThreshold" label="Docker cleanup threshold (%)" required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
@endif
<div class="w-96">
<x-forms.checkbox
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
<ul class='list-disc pl-4 mt-2'>
<li>Removes stopped containers managed by Coolify (as containers are none persistent, no data will be lost).</li>
<li>Deletes unused images.</li>
<li>Clears build cache.</li>
<li>Removes old versions of the Coolify helper image.</li>
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
<li>Optionally remove unused networks (if enabled in advanced options).</li>
</ul>"
instantSave id="forceDockerCleanup" label="Force Docker Cleanup" />
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
<span class="dark:text-warning font-bold">Warning: Enable these
options only if you fully understand their implications and
consequences!</span><br>Improper use will result in data loss and could cause
functional issues.
</p>
<div class="w-96">
<x-forms.checkbox
helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
<x-forms.checkbox instantSave id="deleteUnusedVolumes" label="Delete Unused Volumes"
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Removes stopped containers managed by Coolify (as containers are none persistent, no data will be lost).</li>
<li>Deletes unused images.</li>
<li>Clears build cache.</li>
<li>Removes old versions of the Coolify helper image.</li>
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
<li>Optionally remove unused networks (if enabled in advanced options).</li>
</ul>"
instantSave id="forceDockerCleanup" label="Force Docker Cleanup" />
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
<li>Data from stopped containers volumes will be permanently lost.</li>
<li>No way to recover deleted volume data.</li>
</ul>" />
<x-forms.checkbox instantSave id="deleteUnusedNetworks" label="Delete Unused Networks"
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
<li>Custom networks for stopped containers will be permanently deleted.</li>
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
</ul>" />
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
<span class="dark:text-warning font-bold">Warning: Enable these
options only if you fully understand their implications and
consequences!</span><br>Improper use will result in data loss and could cause
functional issues.
</p>
<div class="w-96">
<x-forms.checkbox instantSave id="deleteUnusedVolumes" label="Delete Unused Volumes"
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
<li>Data from stopped containers volumes will be permanently lost.</li>
<li>No way to recover deleted volume data.</li>
</ul>" />
<x-forms.checkbox instantSave id="deleteUnusedNetworks" label="Delete Unused Networks"
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
<li>Custom networks for stopped containers will be permanently deleted.</li>
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
</ul>" />
</div>
</div>
</form>
<div class="mt-8">
<h3 class="mb-4">Recent executions <span class="text-xs text-neutral-500">(click to check output)</span></h3>

View File

@@ -14,7 +14,7 @@
$traefikDashboardAvailable &&
$server->proxyType() === ProxyTypes::TRAEFIK->value)
<button>
<a wire:navigate target="_blank" href="http://{{ $serverIp }}:8080">
<a target="_blank" href="http://{{ $serverIp }}:8080">
Traefik Dashboard
<x-external-link />
</a>

View File

@@ -37,6 +37,9 @@ services:
- MAILER_USER=${MAILER_USER}
- MAILER_PASSWORD=${MAILER_PASSWORD}
- MAILER_SENDER=${MAILER_SENDER}
- COPILOT_FAL_API_KEY=${COPILOT_FAL_API_KEY}
- COPILOT_PERPLEXITY_API_KEY=${COPILOT_PERPLEXITY_API_KEY}
- COPILOT_OPENAI_API_KEY=${COPILOT_OPENAI_API_KEY}
healthcheck:
test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/3010' || exit 1"]
interval: 5s

View File

@@ -11,3 +11,8 @@ services:
command: tunnel --no-autoupdate run
environment:
- 'TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}'
healthcheck:
test: ["CMD", "cloudflared", "--version"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -0,0 +1,23 @@
# documentation: https://docs.flipt.io/cloud/overview
# slogan: Flipt is a fully managed feature flag solution that enables you to keep your feature flags and remote config next to your code in Git.
# tags: feature flags,devops, CI, CD
# logo: svgs/flipt.svg
# port: 8080
services:
flipt:
image: 'docker.flipt.io/flipt/flipt:latest'
volumes:
- 'flipt-data:/var/opt/flipt'
environment:
- SERVICE_FQDN_FLIPT_8080
healthcheck:
test:
- CMD
- wget
- '--spider'
- '--quiet'
- 'http://127.0.0.1:8080'
interval: 2s
timeout: 10s
retries: 15

View File

@@ -9,24 +9,49 @@ services:
image: invoiceninja/invoiceninja:5
environment:
- SERVICE_FQDN_INVOICENINJA
- APP_NAME=${APP_NAME:-"Invoice Ninja"}
- APP_ENV=${APP_ENV:-production}
- APP_URL=${SERVICE_FQDN_INVOICENINJA}
- APP_KEY=base64:${SERVICE_REALBASE64_INVOICENINJA}
- APP_DEBUG=${APP_DEBUG:-false}
- REQUIRE_HTTPS=${REQUIRE_HTTPS:-false}
- PHANTOMJS_PDF_GENERATION=${PHANTOMJS_PDF_GENERATION:-false}
- PDF_GENERATOR=${PDF_GENERATOR:-snappdf}
- PDF_GENERATOR=${PDF_GENERATOR:-hosted_ninja}
- TRUSTED_PROXIES=${TRUSTED_PROXIES:-*}
- QUEUE_CONNECTION=${QUEUE_CONNECTION:-database}
- IN_USER_EMAIL=${IN_USER_EMAIL:-admin@example.com}
- IN_PASSWORD=${SERVICE_PASSWORD_INVOICENINJAUSER}
- CACHE_DRIVER=redis
- QUEUE_CONNECTION=${QUEUE_CONNECTION:-redis}
- SESSION_DRIVER=redis
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS}
- REDIS_PORT=${REDIS_PORT:-6379}
- DB_HOST=${DB_HOST:-mariadb}
- DB_PORT=${DB_PORT:-3306}
- DB_DATABASE=${DB_DATABASE:-invoiceninja}
- DB_USERNAME=$SERVICE_USER_MARIADB
- DB_PASSWORD=$SERVICE_PASSWORD_MARIADB
- DB_USERNAME=${SERVICE_USER_MARIADB}
- DB_PASSWORD=${SERVICE_PASSWORD_MARIADB}
- IN_USER_EMAIL=${IN_USER_EMAIL:-admin@example.com}
- IN_PASSWORD=${SERVICE_PASSWORD_INVOICENINJAUSER}
- MAIL_MAILER=${MAIL_MAILER:-log}
- MAIL_HOST=${MAIL_HOST}
- MAIL_PORT=${MAIL_PORT}
- MAIL_USERNAME=${MAIL_USERNAME}
- MAIL_PASSWORD=${MAIL_PASSWORD}
- MAIL_ENCRYPTION=${MAIL_ENCRYPTION}
- MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS}
- MAIL_FROM_NAME=${MAIL_FROM_NAME}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
- AWS_BUCKET=${AWS_BUCKET}
- AWS_URL=${AWS_URL}
- AWS_ENDPOINT=${AWS_ENDPOINT}
- NORDIGEN_SECRET_ID=${NORDIGEN_SECRET_ID}
- NORDIGEN_SECRET_KEY=${NORDIGEN_SECRET_KEY}
- IS_DOCKER=true
- SCOUT_DRIVER=${SCOUT_DRIVER}
- LICENSE_KEY=${LICENSE_KEY}
healthcheck:
test: ['CMD', 'echo', 'ok']
test: ["CMD", "echo", "ok"]
interval: 5s
timeout: 20s
retries: 10
@@ -133,12 +158,25 @@ services:
volumes:
- mariadb-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=$SERVICE_PASSWORD_MARIADBROOT
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MARIADBROOT}
- MYSQL_DATABASE=${DB_DATABASE:-invoiceninja}
- MYSQL_USER=$SERVICE_USER_MARIADB
- MYSQL_PASSWORD=$SERVICE_PASSWORD_MARIADB
- MYSQL_USER=${SERVICE_USER_MARIADB}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MARIADB}
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 20s
retries: 10
redis:
image: "redis:7.4-alpine"
command: redis-server --requirepass ${SERVICE_PASSWORD_REDIS}
environment:
- REDIS_PASSWORD=${SERVICE_PASSWORD_REDIS}
volumes:
- "invoice-ninja-redis-data:/data"
healthcheck:
test: ["CMD", "redis-cli", "-a", "${SERVICE_PASSWORD_REDIS}", "ping"]
interval: 10s
timeout: 5s
retries: 5

View File

@@ -25,9 +25,10 @@ services:
- APP_URI=${SERVICE_FQDN_PLUNK}
- API_URI=${SERVICE_FQDN_PLUNK}/api
- DISABLE_SIGNUPS=${DISABLE_SIGNUPS:-False}
- NODE_OPTIONS=--no-network-family-autoselection
entrypoint: [ "/app/entry.sh" ]
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000"]
test: ["CMD-SHELL", "(wget -S --spider http://127.0.0.1:3000/api/health 2>&1 | grep -q \"HTTP/1.1 [1-3]\")"]
interval: 2s
timeout: 10s
retries: 15

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,13 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.383"
"version": "4.0.0-beta.389"
},
"nightly": {
"version": "4.0.0-beta.384"
"version": "4.0.0-beta.390"
},
"helper": {
"version": "1.0.4"
"version": "1.0.5"
},
"realtime": {
"version": "1.0.5"