Merge branch 'next' into fix-volume-variable-with-default

This commit is contained in:
Andras Bacsai
2025-08-17 18:03:38 +02:00
committed by GitHub
346 changed files with 9337 additions and 2595 deletions

View File

@@ -13,6 +13,7 @@ Coolify is an open-source, self-hostable platform for deploying applications and
- `npm run build` - Build frontend assets for production
### Backend Development
Only run artisan commands inside "coolify" container when in development.
- `php artisan serve` - Start Laravel development server
- `php artisan migrate` - Run database migrations
- `php artisan queue:work` - Start queue worker for background jobs

View File

@@ -53,6 +53,7 @@ Thank you so much!
## Big Sponsors
* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy
* [GlueOps](https://www.glueops.dev?ref=coolify.io) - DevOps automation and infrastructure management
* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform
* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
@@ -87,8 +88,11 @@ Thank you so much!
* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
* [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity
## Small Sponsors
<a href="https://open-elements.com/?utm_source=coolify.io"><img width="60px" alt="OpenElements" src="https://github.com/OpenElements.png"/></a>
<a href="https://xaman.app/?utm_source=coolify.io"><img width="60px" alt="XamanApp" src="https://github.com/XamanApp.png"/></a>
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
<a href="https://evercam.io/?utm_source=coolify.io"><img width="60px" alt="Evercam" src="https://github.com/evercam.png"/></a>
<a href="https://github.com/iujlaki"><img width="60px" alt="Imre Ujlaki" src="https://github.com/iujlaki.png"/></a>

View File

@@ -185,6 +185,8 @@ class StartPostgresql
}
}
$command = ['postgres'];
if (filled($this->database->postgres_conf)) {
$docker_compose['services'][$container_name]['volumes'] = array_merge(
$docker_compose['services'][$container_name]['volumes'],
@@ -195,29 +197,25 @@ class StartPostgresql
'read_only' => true,
]]
);
$docker_compose['services'][$container_name]['command'] = [
'postgres',
'-c',
'config_file=/etc/postgresql/postgresql.conf',
];
$command = array_merge($command, ['-c', 'config_file=/etc/postgresql/postgresql.conf']);
}
if ($this->database->enable_ssl) {
$docker_compose['services'][$container_name]['command'] = [
'postgres',
'-c',
'ssl=on',
'-c',
'ssl_cert_file=/var/lib/postgresql/certs/server.crt',
'-c',
'ssl_key_file=/var/lib/postgresql/certs/server.key',
];
$command = array_merge($command, [
'-c', 'ssl=on',
'-c', 'ssl_cert_file=/var/lib/postgresql/certs/server.crt',
'-c', 'ssl_key_file=/var/lib/postgresql/certs/server.key',
]);
}
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
if (count($command) > 1) {
$docker_compose['services'][$container_name]['command'] = $command;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";

View File

@@ -16,7 +16,7 @@ class Services extends Command
/**
* {@inheritdoc}
*/
protected $description = 'Generate service-templates.yaml based on /templates/compose directory';
protected $description = 'Generates service-templates json file based on /templates/compose directory';
public function handle(): int
{
@@ -33,7 +33,10 @@ class Services extends Command
];
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL);
file_put_contents(base_path('templates/'.config('constants.services.file_name')), $serviceTemplatesJson.PHP_EOL);
// Generate service-templates.json with SERVICE_URL changed to SERVICE_FQDN
$this->generateServiceTemplatesWithFqdn();
return self::SUCCESS;
}
@@ -86,4 +89,143 @@ class Services extends Command
return $payload;
}
private function generateServiceTemplatesWithFqdn(): void
{
$serviceTemplatesWithFqdn = collect(array_merge(
glob(base_path('templates/compose/*.yaml')),
glob(base_path('templates/compose/*.yml'))
))
->mapWithKeys(function ($file): array {
$file = basename($file);
$parsed = $this->processFileWithFqdn($file);
return $parsed === false ? [] : [
Arr::pull($parsed, 'name') => $parsed,
];
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesWithFqdn.PHP_EOL);
// Generate service-templates-raw.json with non-base64 encoded compose content
// $this->generateServiceTemplatesRaw();
}
private function processFileWithFqdn(string $file): false|array
{
$content = file_get_contents(base_path("templates/compose/$file"));
$data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array {
preg_match('/^#(?<key>.*):(?<value>.*)$/U', $line, $m);
return $m ? [trim($m['key']) => trim($m['value'])] : [];
});
if (str($data->get('ignore'))->toBoolean()) {
return false;
}
$documentation = $data->get('documentation');
$documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs';
// Replace SERVICE_URL with SERVICE_FQDN in the content
$modifiedContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $content);
$json = Yaml::parse($modifiedContent);
$compose = base64_encode(Yaml::dump($json, 10, 2));
$tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter();
$tags = $tags->isEmpty() ? null : $tags->all();
$payload = [
'name' => pathinfo($file, PATHINFO_FILENAME),
'documentation' => $documentation,
'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose,
'tags' => $tags,
'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
];
if ($port = $data->get('port')) {
$payload['port'] = $port;
}
if ($envFile = $data->get('env_file')) {
$envFileContent = file_get_contents(base_path("templates/compose/$envFile"));
// Also replace SERVICE_URL with SERVICE_FQDN in env file content
$modifiedEnvContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $envFileContent);
$payload['envs'] = base64_encode($modifiedEnvContent);
}
return $payload;
}
private function generateServiceTemplatesRaw(): void
{
$serviceTemplatesRaw = collect(array_merge(
glob(base_path('templates/compose/*.yaml')),
glob(base_path('templates/compose/*.yml'))
))
->mapWithKeys(function ($file): array {
$file = basename($file);
$parsed = $this->processFileWithFqdnRaw($file);
return $parsed === false ? [] : [
Arr::pull($parsed, 'name') => $parsed,
];
})->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents(base_path('templates/service-templates-raw.json'), $serviceTemplatesRaw.PHP_EOL);
}
private function processFileWithFqdnRaw(string $file): false|array
{
$content = file_get_contents(base_path("templates/compose/$file"));
$data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array {
preg_match('/^#(?<key>.*):(?<value>.*)$/U', $line, $m);
return $m ? [trim($m['key']) => trim($m['value'])] : [];
});
if (str($data->get('ignore'))->toBoolean()) {
return false;
}
$documentation = $data->get('documentation');
$documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs';
// Replace SERVICE_URL with SERVICE_FQDN in the content
$modifiedContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $content);
$json = Yaml::parse($modifiedContent);
$compose = Yaml::dump($json, 10, 2); // Not base64 encoded
$tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter();
$tags = $tags->isEmpty() ? null : $tags->all();
$payload = [
'name' => pathinfo($file, PATHINFO_FILENAME),
'documentation' => $documentation,
'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose,
'tags' => $tags,
'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
];
if ($port = $data->get('port')) {
$payload['port'] = $port;
}
if ($envFile = $data->get('env_file')) {
$envFileContent = file_get_contents(base_path("templates/compose/$envFile"));
// Also replace SERVICE_URL with SERVICE_FQDN in env file content (not base64 encoded)
$modifiedEnvContent = str_replace('SERVICE_URL', 'SERVICE_FQDN', $envFileContent);
$payload['envs'] = $modifiedEnvContent;
}
return $payload;
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CheckHelperImageJob;
use App\Jobs\PullChangelogFromGitHub;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\ScheduledDatabaseBackup;
@@ -64,6 +65,7 @@ class Init extends Command
try {
$this->cleanupUnnecessaryDynamicProxyConfiguration();
$this->pullTemplatesFromCDN();
$this->pullChangelogFromGitHub();
} catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
@@ -74,6 +76,7 @@ class Init extends Command
try {
$this->cleanupInProgressApplicationDeployments();
$this->pullTemplatesFromCDN();
$this->pullChangelogFromGitHub();
} catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
@@ -105,7 +108,17 @@ class Init extends Command
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
File::put(base_path('templates/'.config('constants.services.file_name')), json_encode($services));
}
}
private function pullChangelogFromGitHub()
{
try {
PullChangelogFromGitHub::dispatch();
echo "Changelog fetch initiated\n";
} catch (\Throwable $e) {
echo "Could not fetch changelog from GitHub: {$e->getMessage()}\n";
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
class InitChangelog extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'changelog:init {month? : Month in YYYY-MM format (defaults to current month)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Initialize a new monthly changelog file with example structure';
/**
* Execute the console command.
*/
public function handle()
{
$month = $this->argument('month') ?: Carbon::now()->format('Y-m');
// Validate month format
if (! preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) {
$this->error('Invalid month format. Use YYYY-MM format with valid months 01-12 (e.g., 2025-08)');
return self::FAILURE;
}
$changelogsDir = base_path('changelogs');
$filePath = $changelogsDir."/{$month}.json";
// Create changelogs directory if it doesn't exist
if (! is_dir($changelogsDir)) {
mkdir($changelogsDir, 0755, true);
$this->info("Created changelogs directory: {$changelogsDir}");
}
// Check if file already exists
if (file_exists($filePath)) {
if (! $this->confirm("File {$month}.json already exists. Overwrite?")) {
$this->info('Operation cancelled');
return self::SUCCESS;
}
}
// Parse the month for example data
$carbonMonth = Carbon::createFromFormat('Y-m', $month);
$monthName = $carbonMonth->format('F Y');
$sampleDate = $carbonMonth->addDays(14)->toISOString(); // Mid-month
// Get version from config
$version = 'v'.config('constants.coolify.version');
// Create example changelog structure
$exampleData = [
'entries' => [
[
'version' => $version,
'title' => 'Example Feature Release',
'content' => "This is an example changelog entry for {$monthName}. Replace this with your actual release notes. Include details about new features, improvements, bug fixes, and any breaking changes.",
'published_at' => $sampleDate,
],
],
];
// Write the file
$jsonContent = json_encode($exampleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if (file_put_contents($filePath, $jsonContent) === false) {
$this->error("Failed to create changelog file: {$filePath}");
return self::FAILURE;
}
$this->info("✅ Created changelog file: changelogs/{$month}.json");
$this->line(" Example entry created for {$monthName}");
$this->line(' Edit the file to add your actual changelog entries');
// Show the file contents
if ($this->option('verbose')) {
$this->newLine();
$this->line('File contents:');
$this->line($jsonContent);
}
return self::SUCCESS;
}
}

View File

@@ -45,7 +45,7 @@ class SyncBunny extends Command
$install_script = 'install.sh';
$upgrade_script = 'upgrade.sh';
$production_env = '.env.production';
$service_template = 'service-templates.json';
$service_template = config('constants.services.file_name');
$versions = 'versions.json';
$compose_file_location = "$parent_dir/$compose_file";
@@ -102,7 +102,7 @@ class SyncBunny extends Command
}
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
$this->info('About to sync '.config('constants.services.file_name').' to BunnyCDN.');
$confirmed = confirm('Are you sure you want to sync?');
if (! $confirmed) {
return;

View File

@@ -6,6 +6,7 @@ use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CheckHelperImageJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\PullChangelogFromGitHub;
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\RegenerateSslCertJob;
use App\Jobs\ScheduledJobManager;
@@ -67,6 +68,7 @@ class Kernel extends ConsoleKernel
$this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
$this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$this->scheduleInstance->job(new PullChangelogFromGitHub)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->scheduleUpdates();

View File

@@ -447,4 +447,255 @@ class ProjectController extends Controller
return response()->json(['message' => 'Project deleted.']);
}
#[OA\Get(
summary: 'List Environments',
description: 'List all environments in a project.',
path: '/projects/{uuid}/environments',
operationId: 'get-environments',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'List of environments',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Environment')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Project not found.',
),
]
)]
public function get_environments(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Project UUID is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environments = $project->environments()->select('id', 'name', 'uuid')->get();
return response()->json(serializeApiResponse($environments));
}
#[OA\Post(
summary: 'Create Environment',
description: 'Create environment in project.',
path: '/projects/{uuid}/environments',
operationId: 'create-environment',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Environment created.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The name of the environment.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Environment created.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'env123', 'description' => 'The UUID of the environment.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Project not found.',
),
new OA\Response(
response: 409,
description: 'Environment with this name already exists.',
),
]
)]
public function create_environment(Request $request)
{
$allowedFields = ['name'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|required',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (! $request->uuid) {
return response()->json(['message' => 'Project UUID is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$existingEnvironment = $project->environments()->where('name', $request->name)->first();
if ($existingEnvironment) {
return response()->json(['message' => 'Environment with this name already exists.'], 409);
}
$environment = $project->environments()->create([
'name' => $request->name,
]);
return response()->json([
'uuid' => $environment->uuid,
])->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete Environment',
description: 'Delete environment by name or UUID. Environment must be empty.',
path: '/projects/{uuid}/environments/{environment_name_or_uuid}',
operationId: 'delete-environment',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'environment_name_or_uuid', in: 'path', required: true, description: 'Environment name or UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Environment deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Environment deleted.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
description: 'Environment has resources, so it cannot be deleted.',
),
new OA\Response(
response: 404,
description: 'Project or environment not found.',
),
]
)]
public function delete_environment(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Project UUID is required.'], 422);
}
if (! $request->environment_name_or_uuid) {
return response()->json(['message' => 'Environment name or UUID 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_or_uuid)->first();
if (! $environment) {
$environment = $project->environments()->whereUuid($request->environment_name_or_uuid)->first();
}
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
if (! $environment->isEmpty()) {
return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400);
}
$environment->delete();
return response()->json(['message' => 'Environment deleted.']);
}
}

View File

@@ -1428,6 +1428,19 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->pull_request_id !== 0) {
$local_branch = "pull/{$this->pull_request_id}/head";
}
// Build an exact refspec for ls-remote so we don't match similarly named branches (e.g., changeset-release/main)
if ($this->pull_request_id === 0) {
$lsRemoteRef = "refs/heads/{$local_branch}";
} else {
if ($this->git_type === 'github' || $this->git_type === 'gitea') {
$lsRemoteRef = "refs/pull/{$this->pull_request_id}/head";
} elseif ($this->git_type === 'gitlab') {
$lsRemoteRef = "refs/merge-requests/{$this->pull_request_id}/head";
} else {
// Fallback to the original value if provider-specific ref is unknown
$lsRemoteRef = $local_branch;
}
}
$private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) {
$private_key = base64_encode($private_key);
@@ -1442,7 +1455,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
],
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$lsRemoteRef}"),
'hidden' => true,
'save' => 'git_commit_sha',
]
@@ -1450,7 +1463,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$lsRemoteRef}"),
'hidden' => true,
'save' => 'git_commit_sha',
],

View File

@@ -351,6 +351,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$size = $this->calculate_size();
if ($this->backup->save_s3) {
$this->upload_to_s3();
// If local backup is disabled, delete the local file immediately after S3 upload
if ($this->backup->disable_local_backup) {
deleteBackupsLocally($this->backup_location, $this->server);
$this->add_to_backup_output('Local backup file deleted after S3 upload (disable_local_backup enabled).');
}
}
$this->team->notify(new BackupSuccess($this->backup, $this->database, $database));

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
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 PullChangelogFromGitHub implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 30;
public function __construct()
{
$this->onQueue('high');
}
public function handle(): void
{
try {
$response = Http::retry(3, 1000)
->timeout(30)
->get('https://api.github.com/repos/coollabsio/coolify/releases?per_page=10');
if ($response->successful()) {
$releases = $response->json();
$changelog = $this->transformReleasesToChangelog($releases);
// Group entries by month and save them
$this->saveChangelogEntries($changelog);
} else {
send_internal_notification('PullChangelogFromGitHub failed with: '.$response->status().' '.$response->body());
}
} catch (\Throwable $e) {
send_internal_notification('PullChangelogFromGitHub failed with: '.$e->getMessage());
}
}
private function transformReleasesToChangelog(array $releases): array
{
$entries = [];
foreach ($releases as $release) {
// Skip drafts and pre-releases if desired
if ($release['draft']) {
continue;
}
$publishedAt = Carbon::parse($release['published_at']);
$entry = [
'tag_name' => $release['tag_name'],
'title' => $release['name'] ?: $release['tag_name'],
'content' => $release['body'] ?: 'No release notes available.',
'published_at' => $publishedAt->toISOString(),
];
$entries[] = $entry;
}
return $entries;
}
private function saveChangelogEntries(array $entries): void
{
// Create changelogs directory if it doesn't exist
$changelogsDir = base_path('changelogs');
if (! File::exists($changelogsDir)) {
File::makeDirectory($changelogsDir, 0755, true);
}
// Group entries by year-month
$groupedEntries = [];
foreach ($entries as $entry) {
$date = Carbon::parse($entry['published_at']);
$monthKey = $date->format('Y-m');
if (! isset($groupedEntries[$monthKey])) {
$groupedEntries[$monthKey] = [];
}
$groupedEntries[$monthKey][] = $entry;
}
// Save each month's entries to separate files
foreach ($groupedEntries as $month => $monthEntries) {
// Sort entries by published date (newest first)
usort($monthEntries, function ($a, $b) {
return Carbon::parse($b['published_at'])->timestamp - Carbon::parse($a['published_at'])->timestamp;
});
$monthData = [
'entries' => $monthEntries,
'last_updated' => now()->toISOString(),
];
$filePath = base_path("changelogs/{$month}.json");
File::put($filePath, json_encode($monthData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
}
}

View File

@@ -31,7 +31,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
File::put(base_path('templates/'.config('constants.services.file_name')), json_encode($services));
} else {
send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body());
}

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig;
use App\Models\Application;
use App\Models\EnvironmentVariable;
use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\Url\Url;
@@ -228,7 +227,18 @@ class General extends Component
return;
}
$this->application->parse();
// Refresh parsedServiceDomains to reflect any changes in docker_compose_domains
$this->application->refresh();
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
// Convert service names with dots to use underscores for HTML form binding
$sanitizedDomains = [];
foreach ($this->parsedServiceDomains as $serviceName => $domain) {
$sanitizedKey = str($serviceName)->slug('_')->toString();
$sanitizedDomains[$sanitizedKey] = $domain;
}
$this->parsedServiceDomains = $sanitizedDomains;
$showToast && $this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
$this->dispatch('refreshStorages');
@@ -246,7 +256,7 @@ class General extends Component
public function generateDomain(string $serviceName)
{
$uuid = new Cuid2;
$domain = generateFqdn($this->application->destination->server, $uuid);
$domain = generateUrl(server: $this->application->destination->server, random: $uuid);
$sanitizedKey = str($serviceName)->slug('_')->toString();
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;
@@ -270,7 +280,6 @@ class General extends Component
$this->application->save();
$this->dispatch('success', 'Domain generated.');
if ($this->application->build_pack === 'dockercompose') {
$this->updateServiceEnvironmentVariables();
$this->loadComposeFile(showToast: false);
}
@@ -317,7 +326,7 @@ class General extends Component
{
$server = data_get($this->application, 'destination.server');
if ($server) {
$fqdn = generateFqdn($server, $this->application->uuid);
$fqdn = generateFqdn(server: $server, random: $this->application->uuid, parserVersion: $this->application->compose_parsing_version);
$this->application->fqdn = $fqdn;
$this->application->save();
$this->resetDefaultLabels();
@@ -344,7 +353,7 @@ class General extends Component
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->loadComposeFile();
$this->loadComposeFile(showToast: false);
}
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
@@ -421,7 +430,7 @@ class General extends Component
}
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$compose_return = $this->loadComposeFile();
$compose_return = $this->loadComposeFile(showToast: false);
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return;
}
@@ -453,45 +462,23 @@ class General extends Component
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
}
if ($this->application->build_pack === 'dockercompose') {
// Convert sanitized service names back to original names for storage
$originalDomains = [];
foreach ($this->parsedServiceDomains as $key => $value) {
// Find the original service name by checking parsed services
$originalServiceName = $key;
if (isset($this->parsedServices['services'])) {
foreach ($this->parsedServices['services'] as $originalName => $service) {
if (str($originalName)->slug('_')->toString() === $key) {
$originalServiceName = $originalName;
break;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
if ($this->application->isDirty('docker_compose_domains')) {
foreach ($this->parsedServiceDomains as $service) {
$domain = data_get($service, 'domain');
if ($domain) {
if (! validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
}
}
$originalDomains[$originalServiceName] = $value;
}
$this->application->docker_compose_domains = json_encode($originalDomains);
foreach ($originalDomains as $serviceName => $service) {
$domain = data_get($service, 'domain');
if ($domain) {
if (! validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
check_domain_usage(resource: $this->application);
}
}
if ($this->application->isDirty('docker_compose_domains')) {
check_domain_usage(resource: $this->application);
$this->application->save();
$this->resetDefaultLabels();
}
}
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
// Update SERVICE_FQDN_ and SERVICE_URL_ environment variables for Docker Compose applications
if ($this->application->build_pack === 'dockercompose') {
$this->updateServiceEnvironmentVariables();
}
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
@@ -525,25 +512,33 @@ class General extends Component
foreach ($domains as $serviceName => $service) {
$serviceNameFormatted = str($serviceName)->upper()->replace('-', '_');
$domain = data_get($service, 'domain');
// Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed
$this->application->environment_variables()->where('resourceable_type', Application::class)
->where('resourceable_id', $this->application->id)
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
->delete();
$this->application->environment_variables()->where('resourceable_type', Application::class)
->where('resourceable_id', $this->application->id)
->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%")
->delete();
if ($domain) {
// Create or update SERVICE_FQDN_ and SERVICE_URL_ variables
$fqdn = Url::fromString($domain);
$port = $fqdn->getPort();
$path = $fqdn->getPath();
$fqdnValue = $fqdn->getScheme().'://'.$fqdn->getHost();
if ($path !== '/') {
$fqdnValue = $fqdnValue.$path;
}
$urlValue = str($domain)->after('://');
$urlValue = $fqdn->getScheme().'://'.$fqdn->getHost();
if ($path !== '/') {
$urlValue = $urlValue.$path;
}
$fqdnValue = str($domain)->after('://');
if ($path !== '/') {
$fqdnValue = $fqdnValue.$path;
}
// Create/update SERVICE_FQDN_
EnvironmentVariable::updateOrCreate([
'resourceable_type' => Application::class,
'resourceable_id' => $this->application->id,
$this->application->environment_variables()->updateOrCreate([
'key' => "SERVICE_FQDN_{$serviceNameFormatted}",
], [
'value' => $fqdnValue,
@@ -552,21 +547,16 @@ class General extends Component
]);
// Create/update SERVICE_URL_
EnvironmentVariable::updateOrCreate([
'resourceable_type' => Application::class,
'resourceable_id' => $this->application->id,
$this->application->environment_variables()->updateOrCreate([
'key' => "SERVICE_URL_{$serviceNameFormatted}",
], [
'value' => $urlValue,
'is_build_time' => false,
'is_preview' => false,
]);
// Create/update port-specific variables if port exists
if ($port) {
EnvironmentVariable::updateOrCreate([
'resourceable_type' => Application::class,
'resourceable_id' => $this->application->id,
if (filled($port)) {
$this->application->environment_variables()->updateOrCreate([
'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}",
], [
'value' => $fqdnValue,
@@ -574,9 +564,7 @@ class General extends Component
'is_preview' => false,
]);
EnvironmentVariable::updateOrCreate([
'resourceable_type' => Application::class,
'resourceable_id' => $this->application->id,
$this->application->environment_variables()->updateOrCreate([
'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}",
], [
'value' => $urlValue,
@@ -584,17 +572,6 @@ class General extends Component
'is_preview' => false,
]);
}
} else {
// Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed
EnvironmentVariable::where('resourceable_type', Application::class)
->where('resourceable_id', $this->application->id)
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
->delete();
EnvironmentVariable::where('resourceable_type', Application::class)
->where('resourceable_id', $this->application->id)
->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%")
->delete();
}
}
}

View File

@@ -48,7 +48,7 @@ class PreviewsCompose extends Component
$random = new Cuid2;
// Generate a unique domain like main app services do
$generated_fqdn = generateFqdn($server, $random);
$generated_fqdn = generateFqdn(server: $server, random: $random, parserVersion: $this->preview->application->compose_parsing_version);
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn);

View File

@@ -129,7 +129,7 @@ class CloneMe extends Component
$uuid = (string) new Cuid2;
$url = $application->fqdn;
if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$url = generateFqdn($this->server, $uuid);
$url = generateFqdn(server: $this->server, random: $uuid, parserVersion: $application->compose_parsing_version);
}
$newApplication = $application->replicate([

View File

@@ -64,6 +64,9 @@ class BackupEdit extends Component
#[Validate(['required', 'boolean'])]
public bool $saveS3 = false;
#[Validate(['required', 'boolean'])]
public bool $disableLocalBackup = false;
#[Validate(['nullable', 'integer'])]
public ?int $s3StorageId = 1;
@@ -98,6 +101,7 @@ class BackupEdit extends Component
$this->backup->database_backup_retention_days_s3 = $this->databaseBackupRetentionDaysS3;
$this->backup->database_backup_retention_max_storage_s3 = $this->databaseBackupRetentionMaxStorageS3;
$this->backup->save_s3 = $this->saveS3;
$this->backup->disable_local_backup = $this->disableLocalBackup;
$this->backup->s3_storage_id = $this->s3StorageId;
$this->backup->databases_to_backup = $this->databasesToBackup;
$this->backup->dump_all = $this->dumpAll;
@@ -115,6 +119,7 @@ class BackupEdit extends Component
$this->databaseBackupRetentionDaysS3 = $this->backup->database_backup_retention_days_s3;
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
$this->saveS3 = $this->backup->save_s3;
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
$this->s3StorageId = $this->backup->s3_storage_id;
$this->databasesToBackup = $this->backup->databases_to_backup;
$this->dumpAll = $this->backup->dump_all;
@@ -193,6 +198,12 @@ class BackupEdit extends Component
if (! is_numeric($this->backup->s3_storage_id)) {
$this->backup->s3_storage_id = null;
}
// Validate that disable_local_backup can only be true when S3 backup is enabled
if ($this->backup->disable_local_backup && ! $this->backup->save_s3) {
throw new \Exception('Local backup can only be disabled when S3 backup is enabled.');
}
$isValid = validate_cron_expression($this->backup->frequency);
if (! $isValid) {
throw new \Exception('Invalid Cron / Human expression');

View File

@@ -132,6 +132,12 @@ class ExecuteContainerCommand extends Component
});
}
}
// Sort containers alphabetically by name
$this->containers = $this->containers->sortBy(function ($container) {
return data_get($container, 'container.Names');
});
if ($this->containers->count() === 1) {
$this->selected_container = data_get($this->containers->first(), 'container.Names');
}

View File

@@ -61,7 +61,7 @@ class ResourceOperations extends Component
$url = $this->resource->fqdn;
if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$url = generateFqdn($server, $uuid);
$url = generateFqdn(server: $server, random: $uuid, parserVersion: $this->resource->compose_parsing_version);
}
$new_resource = $this->resource->replicate([

View File

@@ -6,6 +6,7 @@ use App\Actions\Server\CheckUpdates;
use App\Actions\Server\UpdatePackage;
use App\Events\ServerPackageUpdated;
use App\Models\Server;
use App\Notifications\Server\ServerPatchCheck;
use Livewire\Component;
class Patches extends Component
@@ -96,6 +97,89 @@ class Patches extends Component
}
}
public function sendTestEmail()
{
if (! isDev()) {
$this->dispatch('error', message: 'Test email functionality is only available in development mode.');
return;
}
try {
// Get current patch data or create test data if none exists
$testPatchData = $this->createTestPatchData();
// Send test notification
$this->server->team->notify(new ServerPatchCheck($this->server, $testPatchData));
$this->dispatch('success', 'Test email sent successfully! Check your email inbox.');
} catch (\Exception $e) {
$this->dispatch('error', message: 'Failed to send test email: '.$e->getMessage());
}
}
private function createTestPatchData(): array
{
// If we have real patch data, use it
if (isset($this->updates) && is_array($this->updates) && count($this->updates) > 0) {
return [
'total_updates' => $this->totalUpdates,
'updates' => $this->updates,
'osId' => $this->osId,
'package_manager' => $this->packageManager,
];
}
// Otherwise create realistic test data
return [
'total_updates' => 8,
'updates' => [
[
'package' => 'docker-ce',
'current_version' => '24.0.7-1',
'new_version' => '25.0.1-1',
],
[
'package' => 'nginx',
'current_version' => '1.20.2-1',
'new_version' => '1.22.1-1',
],
[
'package' => 'kernel-generic',
'current_version' => '5.15.0-89',
'new_version' => '5.15.0-91',
],
[
'package' => 'openssh-server',
'current_version' => '8.9p1-3',
'new_version' => '9.0p1-1',
],
[
'package' => 'curl',
'current_version' => '7.81.0-1',
'new_version' => '7.85.0-1',
],
[
'package' => 'git',
'current_version' => '2.34.1-1',
'new_version' => '2.39.1-1',
],
[
'package' => 'python3',
'current_version' => '3.10.6-1',
'new_version' => '3.11.0-1',
],
[
'package' => 'htop',
'current_version' => '3.2.1-1',
'new_version' => '3.2.2-1',
],
],
'osId' => $this->osId ?? 'ubuntu',
'package_manager' => $this->packageManager ?? 'apt',
];
}
public function render()
{
return view('livewire.server.security.patches');

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Livewire;
use App\Jobs\PullChangelogFromGitHub;
use App\Services\ChangelogService;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class SettingsDropdown extends Component
{
public $showWhatsNewModal = false;
public function getUnreadCountProperty()
{
return Auth::user()->getUnreadChangelogCount();
}
public function getEntriesProperty()
{
$user = Auth::user();
return app(ChangelogService::class)->getEntriesForUser($user);
}
public function openWhatsNewModal()
{
$this->showWhatsNewModal = true;
}
public function closeWhatsNewModal()
{
$this->showWhatsNewModal = false;
}
public function markAsRead($identifier)
{
app(ChangelogService::class)->markAsReadForUser($identifier, Auth::user());
}
public function markAllAsRead()
{
app(ChangelogService::class)->markAllAsReadForUser(Auth::user());
}
public function manualFetchChangelog()
{
if (! isDev()) {
return;
}
try {
PullChangelogFromGitHub::dispatch();
$this->dispatch('success', 'Changelog fetch initiated! Check back in a few moments.');
} catch (\Throwable $e) {
$this->dispatch('error', 'Failed to fetch changelog: '.$e->getMessage());
}
}
public function render()
{
return view('livewire.settings-dropdown', [
'entries' => $this->entries,
'unreadCount' => $this->unreadCount,
]);
}
}

View File

@@ -75,7 +75,7 @@ class Index extends Component
}
} catch (\Exception $e) {
// Log the error
logger()->error('Stripe API error: ' . $e->getMessage());
logger()->error('Stripe API error: '.$e->getMessage());
// Set a flag to show an error message to the user
$this->addError('stripe', 'Could not retrieve subscription information. Please try again later.');
} finally {

View File

@@ -59,7 +59,7 @@ class Index extends Component
return null;
})->filter();
});
})->sortBy('name');
}
public function updatedSelectedUuid()

View File

@@ -111,7 +111,7 @@ class Application extends BaseModel
{
use HasConfiguration, HasFactory, SoftDeletes;
private static $parserVersion = '4';
private static $parserVersion = '5';
protected $guarded = [];
@@ -1353,7 +1353,7 @@ class Application extends BaseModel
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{
if ((int) $this->compose_parsing_version >= 3) {
return newParser($this, $pull_request_id, $preview_id);
return applicationParser($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 {
@@ -1442,7 +1442,21 @@ class Application extends BaseModel
$parsedServices = $this->parse();
if ($this->docker_compose_domains) {
$json = collect(json_decode($this->docker_compose_domains));
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
foreach ($json as $key => $value) {
if (str($key)->contains('-')) {
$key = str($key)->replace('-', '_');
}
$json->put((string) $key, $value);
}
$services = collect(data_get($parsedServices, 'services', []));
foreach ($services as $name => $service) {
if (str($name)->contains('-')) {
$replacedName = str($name)->replace('-', '_');
$services->put((string) $replacedName, $service);
$services->forget((string) $name);
}
}
$names = collect($services)->keys()->toArray();
$jsonNames = $json->keys()->toArray();
$diff = array_diff($jsonNames, $names);
$json = $json->filter(function ($value, $key) use ($diff) {

View File

@@ -74,7 +74,7 @@ class ApplicationPreview extends BaseModel
public function generate_preview_fqdn()
{
if (is_null($this->fqdn) && $this->application->fqdn) {
if (empty($this->fqdn) && $this->application->fqdn) {
if (str($this->application->fqdn)->contains(',')) {
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);

View File

@@ -42,7 +42,7 @@ class Service extends BaseModel
{
use HasFactory, SoftDeletes;
private static $parserVersion = '4';
private static $parserVersion = '5';
protected $guarded = [];
@@ -255,6 +255,19 @@ class Service extends BaseModel
continue;
}
switch ($image) {
case $image->contains('drizzle-team/gateway'):
$data = collect([]);
$masterpass = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_DRIZZLE')->first();
$data = $data->merge([
'Master Password' => [
'key' => data_get($masterpass, 'key'),
'value' => data_get($masterpass, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
$fields->put('Drizzle', $data->toArray());
break;
case $image->contains('castopod'):
$data = collect([]);
$disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first();
@@ -1277,7 +1290,7 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
if ((int) $this->compose_parsing_version >= 3) {
return newParser($this);
return serviceParser($this);
} elseif ($this->docker_compose_raw) {
return parseDockerComposeFile($this, $isNew);
} else {

View File

@@ -203,6 +203,16 @@ class User extends Authenticatable implements SendsEmail
return $this->belongsToMany(Team::class)->withPivot('role');
}
public function changelogReads()
{
return $this->hasMany(UserChangelogRead::class);
}
public function getUnreadChangelogCount(): int
{
return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this);
}
public function getRecipients(): array
{
return [$this->email];

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserChangelogRead extends Model
{
protected $fillable = [
'user_id',
'release_tag',
'read_at',
];
protected $casts = [
'read_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public static function markAsRead(int $userId, string $identifier): void
{
self::firstOrCreate([
'user_id' => $userId,
'release_tag' => $identifier,
], [
'read_at' => now(),
]);
}
public static function isReadByUser(int $userId, string $identifier): bool
{
return self::where('user_id', $userId)
->where('release_tag', $identifier)
->exists();
}
public static function getReadIdentifiersForUser(int $userId): array
{
return self::where('user_id', $userId)
->pluck('release_tag')
->toArray();
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Notifications\Channels;
use App\Models\Team;
use Exception;
use Illuminate\Notifications\Notification;
use Resend;
@@ -11,60 +13,102 @@ class EmailChannel
public function send(SendsEmail $notifiable, Notification $notification): void
{
$useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings;
$isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false);
$customEmails = data_get($notification, 'emails', null);
if ($useInstanceEmailSettings || $isTransactionalEmail) {
$settings = instanceSettings();
} else {
$settings = $notifiable->emailNotificationSettings;
}
$isResendEnabled = $settings->resend_enabled;
$isSmtpEnabled = $settings->smtp_enabled;
if ($customEmails) {
$recipients = [$customEmails];
} else {
$recipients = $notifiable->getRecipients();
}
$mailMessage = $notification->toMail($notifiable);
try {
// Get team and validate membership before proceeding
$team = data_get($notifiable, 'id');
$members = Team::find($team)->members;
if ($isResendEnabled) {
$resend = Resend::client($settings->resend_api_key);
$from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>";
$resend->emails->send([
'from' => $from,
'to' => $recipients,
'subject' => $mailMessage->subject,
'html' => (string) $mailMessage->render(),
$useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings;
$isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false);
$customEmails = data_get($notification, 'emails', null);
if ($useInstanceEmailSettings || $isTransactionalEmail) {
$settings = instanceSettings();
} else {
$settings = $notifiable->emailNotificationSettings;
}
$isResendEnabled = $settings->resend_enabled;
$isSmtpEnabled = $settings->smtp_enabled;
if ($customEmails) {
$recipients = [$customEmails];
} else {
$recipients = $notifiable->getRecipients();
}
// Validate team membership for all recipients
if (count($recipients) === 0) {
throw new Exception('No email recipients found');
}
foreach ($recipients as $recipient) {
// Check if the recipient is part of the team
if (! $members->contains('email', $recipient)) {
$emailSettings = $notifiable->emailNotificationSettings;
data_set($emailSettings, 'smtp_password', '********');
data_set($emailSettings, 'resend_api_key', '********');
send_internal_notification(sprintf(
"Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s",
$recipient,
$team,
get_class($notification),
get_class($notifiable),
json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
));
throw new Exception('Recipient is not part of the team');
}
}
$mailMessage = $notification->toMail($notifiable);
if ($isResendEnabled) {
$resend = Resend::client($settings->resend_api_key);
$from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>";
$resend->emails->send([
'from' => $from,
'to' => $recipients,
'subject' => $mailMessage->subject,
'html' => (string) $mailMessage->render(),
]);
} elseif ($isSmtpEnabled) {
$encryption = match (strtolower($settings->smtp_encryption)) {
'starttls' => null,
'tls' => 'tls',
'none' => null,
default => null,
};
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
$settings->smtp_host,
$settings->smtp_port,
$encryption
);
$transport->setUsername($settings->smtp_username ?? '');
$transport->setPassword($settings->smtp_password ?? '');
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromEmail = $settings->smtp_from_address ?? 'noreply@localhost';
$fromName = $settings->smtp_from_name ?? 'System';
$from = new \Symfony\Component\Mime\Address($fromEmail, $fromName);
$email = (new \Symfony\Component\Mime\Email)
->from($from)
->to(...$recipients)
->subject($mailMessage->subject)
->html((string) $mailMessage->render());
$mailer->send($email);
}
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('EmailChannel failed: '.$e->getMessage(), [
'notification' => get_class($notification),
'notifiable' => get_class($notifiable),
'team_id' => data_get($notifiable, 'id'),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} elseif ($isSmtpEnabled) {
$encryption = match (strtolower($settings->smtp_encryption)) {
'starttls' => null,
'tls' => 'tls',
'none' => null,
default => null,
};
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
$settings->smtp_host,
$settings->smtp_port,
$encryption
);
$transport->setUsername($settings->smtp_username ?? '');
$transport->setPassword($settings->smtp_password ?? '');
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromEmail = $settings->smtp_from_address ?? 'noreply@localhost';
$fromName = $settings->smtp_from_name ?? 'System';
$from = new \Symfony\Component\Mime\Address($fromEmail, $fromName);
$email = (new \Symfony\Component\Mime\Email)
->from($from)
->to(...$recipients)
->subject($mailMessage->subject)
->html((string) $mailMessage->render());
$mailer->send($email);
throw $e;
}
}
}

View File

@@ -21,7 +21,7 @@ class S3StoragePolicy
*/
public function view(User $user, S3Storage $storage): bool
{
return $user->teams()->where('id', $storage->team_id)->exists();
return $user->teams()->get()->firstWhere('id', $storage->team_id)->exists();
}
/**
@@ -37,7 +37,7 @@ class S3StoragePolicy
*/
public function update(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
return $user->teams()->get()->firstWhere('id', $server->team_id)->exists() && $user->isAdmin();
}
/**
@@ -45,7 +45,7 @@ class S3StoragePolicy
*/
public function delete(User $user, S3Storage $storage): bool
{
return $user->teams()->where('id', $storage->team_id)->exists();
return $user->teams()->get()->firstWhere('id', $storage->team_id)->exists() && $user->isAdmin();
}
/**

View File

@@ -0,0 +1,300 @@
<?php
namespace App\Services;
use App\Models\User;
use App\Models\UserChangelogRead;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Spatie\LaravelMarkdown\MarkdownRenderer;
class ChangelogService
{
public function getEntries(int $recentMonths = 3): Collection
{
// For backward compatibility, check if old changelog.json exists
if (file_exists(base_path('changelog.json'))) {
$data = $this->fetchChangelogData();
if (! $data || ! isset($data['entries'])) {
return collect();
}
return collect($data['entries'])
->filter(fn ($entry) => $this->validateEntryData($entry))
->map(function ($entry) {
$entry['published_at'] = Carbon::parse($entry['published_at']);
$entry['content_html'] = $this->parseMarkdown($entry['content']);
return (object) $entry;
})
->filter(fn ($entry) => $entry->published_at <= now())
->sortBy('published_at')
->reverse()
->values();
}
// Load entries from recent months for performance
$availableMonths = $this->getAvailableMonths();
$monthsToLoad = $availableMonths->take($recentMonths);
return $monthsToLoad
->flatMap(fn ($month) => $this->getEntriesForMonth($month))
->sortBy('published_at')
->reverse()
->values();
}
public function getAllEntries(): Collection
{
$availableMonths = $this->getAvailableMonths();
return $availableMonths
->flatMap(fn ($month) => $this->getEntriesForMonth($month))
->sortBy('published_at')
->reverse()
->values();
}
public function getEntriesForUser(User $user): Collection
{
$entries = $this->getEntries();
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
return $entries->map(function ($entry) use ($readIdentifiers) {
$entry->is_read = in_array($entry->tag_name, $readIdentifiers);
return $entry;
})->sortBy([
['is_read', 'asc'], // unread first
['published_at', 'desc'], // then by date
])->values();
}
public function getUnreadCountForUser(User $user): int
{
if (isDev()) {
$entries = $this->getEntries();
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count();
} else {
return Cache::remember(
'user_unread_changelog_count_'.$user->id,
now()->addHour(),
function () use ($user) {
$entries = $this->getEntries();
$readIdentifiers = UserChangelogRead::getReadIdentifiersForUser($user->id);
return $entries->reject(fn ($entry) => in_array($entry->tag_name, $readIdentifiers))->count();
}
);
}
}
public function getAvailableMonths(): Collection
{
$pattern = base_path('changelogs/*.json');
$files = glob($pattern);
if ($files === false) {
return collect();
}
return collect($files)
->map(fn ($file) => basename($file, '.json'))
->filter(fn ($name) => preg_match('/^\d{4}-\d{2}$/', $name))
->sort()
->reverse()
->values();
}
public function getEntriesForMonth(string $month): Collection
{
$path = base_path("changelogs/{$month}.json");
if (! file_exists($path)) {
return collect();
}
$content = file_get_contents($path);
if ($content === false) {
Log::error("Failed to read changelog file: {$month}.json");
return collect();
}
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Log::error("Invalid JSON in {$month}.json: ".json_last_error_msg());
return collect();
}
if (! isset($data['entries']) || ! is_array($data['entries'])) {
return collect();
}
return collect($data['entries'])
->filter(fn ($entry) => $this->validateEntryData($entry))
->map(function ($entry) {
$entry['published_at'] = Carbon::parse($entry['published_at']);
$entry['content_html'] = $this->parseMarkdown($entry['content']);
return (object) $entry;
})
->filter(fn ($entry) => $entry->published_at <= now())
->sortBy('published_at')
->reverse()
->values();
}
private function fetchChangelogData(): ?array
{
// Legacy support for old changelog.json
$path = base_path('changelog.json');
if (file_exists($path)) {
$content = file_get_contents($path);
if ($content === false) {
Log::error('Failed to read changelog.json file');
return null;
}
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Log::error('Invalid JSON in changelog.json: '.json_last_error_msg());
return null;
}
return $data;
}
// New monthly structure - combine all months
$allEntries = [];
foreach ($this->getAvailableMonths() as $month) {
$monthEntries = $this->getEntriesForMonth($month);
foreach ($monthEntries as $entry) {
$allEntries[] = (array) $entry;
}
}
return ['entries' => $allEntries];
}
public function markAsReadForUser(string $version, User $user): void
{
UserChangelogRead::markAsRead($user->id, $version);
Cache::forget('user_unread_changelog_count_'.$user->id);
}
public function markAllAsReadForUser(User $user): void
{
$entries = $this->getEntries();
foreach ($entries as $entry) {
UserChangelogRead::markAsRead($user->id, $entry->tag_name);
}
Cache::forget('user_unread_changelog_count_'.$user->id);
}
private function validateEntryData(array $data): bool
{
$required = ['tag_name', 'title', 'content', 'published_at'];
foreach ($required as $field) {
if (! isset($data[$field]) || empty($data[$field])) {
return false;
}
}
return true;
}
public function clearAllReadStatus(): array
{
try {
$count = UserChangelogRead::count();
UserChangelogRead::truncate();
// Clear all user caches
$this->clearAllUserCaches();
return [
'success' => true,
'message' => "Successfully cleared {$count} read status records",
];
} catch (\Exception $e) {
Log::error('Failed to clear read status: '.$e->getMessage());
return [
'success' => false,
'message' => 'Failed to clear read status: '.$e->getMessage(),
];
}
}
private function clearAllUserCaches(): void
{
$users = User::select('id')->get();
foreach ($users as $user) {
Cache::forget('user_unread_changelog_count_'.$user->id);
}
}
private function parseMarkdown(string $content): string
{
$renderer = app(MarkdownRenderer::class);
$html = $renderer->toHtml($content);
// Apply custom Tailwind CSS classes for dark mode compatibility
$html = $this->applyCustomStyling($html);
return $html;
}
private function applyCustomStyling(string $html): string
{
// Headers
$html = preg_replace('/<h1[^>]*>/', '<h1 class="text-xl font-bold dark:text-white mb-2">', $html);
$html = preg_replace('/<h2[^>]*>/', '<h2 class="text-lg font-semibold dark:text-white mb-2">', $html);
$html = preg_replace('/<h3[^>]*>/', '<h3 class="text-md font-semibold dark:text-white mb-1">', $html);
// Paragraphs
$html = preg_replace('/<p[^>]*>/', '<p class="mb-2 dark:text-neutral-300">', $html);
// Lists
$html = preg_replace('/<ul[^>]*>/', '<ul class="mb-2 ml-4 list-disc">', $html);
$html = preg_replace('/<ol[^>]*>/', '<ol class="mb-2 ml-4 list-decimal">', $html);
$html = preg_replace('/<li[^>]*>/', '<li class="dark:text-neutral-300">', $html);
// Code blocks and inline code
$html = preg_replace('/<pre[^>]*>/', '<pre class="bg-gray-100 dark:bg-coolgray-300 p-2 rounded text-sm overflow-x-auto my-2">', $html);
$html = preg_replace('/<code[^>]*>/', '<code class="bg-gray-100 dark:bg-coolgray-300 px-1 py-0.5 rounded text-sm">', $html);
// Links - Apply styling to existing markdown links
$html = preg_replace('/<a([^>]*)>/', '<a$1 class="text-blue-500 hover:text-blue-600 underline" target="_blank" rel="noopener">', $html);
// Convert plain URLs to clickable links (that aren't already in <a> tags)
$html = preg_replace('/(?<!href="|href=\')(?<!>)(?<!\/)(https?:\/\/[^\s<>"]+)(?![^<]*<\/a>)/', '<a href="$1" class="text-blue-500 hover:text-blue-600 underline" target="_blank" rel="noopener">$1</a>', $html);
// Strong/bold text
$html = preg_replace('/<strong[^>]*>/', '<strong class="font-semibold dark:text-white">', $html);
// Emphasis/italic text
$html = preg_replace('/<em[^>]*>/', '<em class="italic dark:text-neutral-300">', $html);
return $html;
}
}

View File

@@ -25,6 +25,7 @@ class Input extends Component
public string $autocomplete = 'off',
public ?int $minlength = null,
public ?int $maxlength = null,
public bool $autofocus = false,
) {}
public function render(): View|Closure|string

View File

@@ -237,11 +237,18 @@ function removeOldBackups($backup): void
{
try {
if ($backup->executions) {
$localBackupsToDelete = deleteOldBackupsLocally($backup);
if ($localBackupsToDelete->isNotEmpty()) {
// If local backup is disabled, mark all executions as having local storage deleted
if ($backup->disable_local_backup && $backup->save_s3) {
$backup->executions()
->whereIn('id', $localBackupsToDelete->pluck('id'))
->where('local_storage_deleted', false)
->update(['local_storage_deleted' => true]);
} else {
$localBackupsToDelete = deleteOldBackupsLocally($backup);
if ($localBackupsToDelete->isNotEmpty()) {
$backup->executions()
->whereIn('id', $localBackupsToDelete->pluck('id'))
->update(['local_storage_deleted' => true]);
}
}
}
@@ -254,10 +261,18 @@ function removeOldBackups($backup): void
}
}
$backup->executions()
->where('local_storage_deleted', true)
->where('s3_storage_deleted', true)
->delete();
// Delete executions where both local and S3 storage are marked as deleted
// or where only S3 is enabled and S3 storage is deleted
if ($backup->disable_local_backup && $backup->save_s3) {
$backup->executions()
->where('s3_storage_deleted', true)
->delete();
} else {
$backup->executions()
->where('local_storage_deleted', true)
->where('s3_storage_deleted', true)
->delete();
}
} catch (\Exception $e) {
throw $e;

View File

@@ -256,12 +256,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) {
$MINIO_BROWSER_REDIRECT_URL->update([
'value' => generateFqdn($server, 'console-'.$uuid, true),
'value' => generateFqdn(server: $server, random: 'console-'.$uuid, parserVersion: $resource->compose_parsing_version, forceHttps: true),
]);
}
if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) {
$MINIO_SERVER_URL->update([
'value' => generateFqdn($server, 'minio-'.$uuid, true),
'value' => generateFqdn(server: $server, random: 'minio-'.$uuid, parserVersion: $resource->compose_parsing_version, forceHttps: true),
]);
}
$payload = collect([
@@ -279,12 +279,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) {
$LOGTO_ENDPOINT->update([
'value' => generateFqdn($server, 'logto-'.$uuid),
'value' => generateFqdn(server: $server, random: 'logto-'.$uuid, parserVersion: $resource->compose_parsing_version),
]);
}
if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) {
$LOGTO_ADMIN_ENDPOINT->update([
'value' => generateFqdn($server, 'logto-admin-'.$uuid),
'value' => generateFqdn(server: $server, random: 'logto-admin-'.$uuid, parserVersion: $resource->compose_parsing_version),
]);
}
$payload = collect([
@@ -1101,7 +1101,7 @@ function getContainerLogs(Server $server, string $container_id, int $lines = 100
], $server);
}
$output .= removeAnsiColors($output);
$output = removeAnsiColors($output);
return $output;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
<?php
use App\Models\Application;
use App\Models\EnvironmentVariable;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -115,47 +114,32 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$resource->image = $updatedImage;
$resource->save();
}
$serviceName = str($resource->name)->upper()->replace('-', '_');
$resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")->delete();
$resource->service->environment_variables()->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")->delete();
if ($resource->fqdn) {
$resourceFqdns = str($resource->fqdn)->explode(',');
if ($resourceFqdns->count() === 1) {
$resourceFqdns = $resourceFqdns->first();
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_');
$fqdn = Url::fromString($resourceFqdns);
$port = $fqdn->getPort();
$path = $fqdn->getPath();
$fqdn = $fqdn->getScheme().'://'.$fqdn->getHost();
$fqdnValue = ($path === '/') ? $fqdn : $fqdn.$path;
EnvironmentVariable::updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
'key' => $variableName,
], [
'value' => $fqdnValue,
'is_build_time' => false,
'is_preview' => false,
]);
if ($port) {
$variableName = $variableName."_$port";
EnvironmentVariable::updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
'key' => $variableName,
], [
'value' => $fqdnValue,
'is_build_time' => false,
'is_preview' => false,
]);
}
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
$url = Url::fromString($fqdn);
$port = $url->getPort();
$path = $url->getPath();
$url = $url->getHost();
$urlValue = str($fqdn)->after('://');
if ($path !== '/') {
$urlValue = $urlValue.$path;
}
EnvironmentVariable::updateOrCreate([
$resourceFqdns = $resourceFqdns->first();
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
$url = Url::fromString($resourceFqdns);
$port = $url->getPort();
$path = $url->getPath();
$urlValue = $url->getScheme().'://'.$url->getHost();
$urlValue = ($path === '/') ? $urlValue : $urlValue.$path;
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
'key' => $variableName,
], [
'value' => $urlValue,
'is_build_time' => false,
'is_preview' => false,
]);
if ($port) {
$variableName = $variableName."_$port";
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
'key' => $variableName,
@@ -164,114 +148,37 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
'is_build_time' => false,
'is_preview' => false,
]);
if ($port) {
$variableName = $variableName."_$port";
EnvironmentVariable::updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
'key' => $variableName,
], [
'value' => $urlValue,
'is_build_time' => false,
'is_preview' => false,
]);
}
} elseif ($resourceFqdns->count() > 1) {
foreach ($resourceFqdns as $fqdn) {
$host = Url::fromString($fqdn);
$port = $host->getPort();
$url = $host->getHost();
$path = $host->getPath();
$host = $host->getScheme().'://'.$host->getHost();
if ($port) {
$port_envs = EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', 'like', "SERVICE_FQDN_%_$port")
->get();
foreach ($port_envs as $port_env) {
$service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_');
$env = EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', 'SERVICE_FQDN_'.$service_fqdn)
->first();
if ($env) {
if ($path === '/') {
$env->value = $host;
} else {
$env->value = $host.$path;
}
$env->save();
}
if ($path === '/') {
$port_env->value = $host;
} else {
$port_env->value = $host.$path;
}
$port_env->save();
}
$port_envs_url = EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', 'like', "SERVICE_URL_%_$port")
->get();
foreach ($port_envs_url as $port_env_url) {
$service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_');
$env = EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', 'SERVICE_URL_'.$service_url)
->first();
if ($env) {
if ($path === '/') {
$env->value = $url;
} else {
$env->value = $url.$path;
}
$env->save();
}
if ($path === '/') {
$port_env_url->value = $url;
} else {
$port_env_url->value = $url.$path;
}
$port_env_url->save();
}
} else {
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_');
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', $variableName)
->first();
$fqdn = Url::fromString($fqdn);
$fqdn = $fqdn->getScheme().'://'.$fqdn->getHost().$fqdn->getPath();
if ($generatedEnv) {
$generatedEnv->value = $fqdn;
$generatedEnv->save();
}
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', $variableName)
->first();
$url = Url::fromString($fqdn);
$url = $url->getHost().$url->getPath();
if ($generatedEnv) {
$url = str($fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
}
}
}
}
} else {
// If FQDN is removed, delete the corresponding environment variables
$serviceName = str($resource->name)->upper()->replace('-', '_');
EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")
->delete();
EnvironmentVariable::where('resourceable_type', Service::class)
->where('resourceable_id', $resource->service_id)
->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")
->delete();
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_');
$fqdn = Url::fromString($resourceFqdns);
$port = $fqdn->getPort();
$path = $fqdn->getPath();
$fqdn = $fqdn->getHost();
$fqdnValue = str($fqdn)->after('://');
if ($path !== '/') {
$fqdnValue = $fqdnValue.$path;
}
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
'key' => $variableName,
], [
'value' => $fqdnValue,
'is_build_time' => false,
'is_preview' => false,
]);
if ($port) {
$variableName = $variableName."_$port";
$resource->service->environment_variables()->updateOrCreate([
'resourceable_type' => Service::class,
'resourceable_id' => $resource->service_id,
'key' => $variableName,
], [
'value' => $fqdnValue,
'is_build_time' => false,
'is_preview' => false,
]);
}
}
} catch (\Throwable $e) {
return handleError($e);

File diff suppressed because it is too large Load Diff

2
changelogs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -47,6 +47,7 @@
"socialiteproviders/zitadel": "^4.2",
"spatie/laravel-activitylog": "^4.10.2",
"spatie/laravel-data": "^4.17.0",
"spatie/laravel-markdown": "^2.7",
"spatie/laravel-ray": "^1.40.2",
"spatie/laravel-schemaless-attributes": "^2.5.1",
"spatie/url": "^2.4",

203
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "52a680a0eb446dcaa74bc35e158aca57",
"content-hash": "a78cf8fdfec25eac43de77c05640dc91",
"packages": [
{
"name": "amphp/amp",
@@ -7902,6 +7902,66 @@
],
"time": "2025-05-08T15:41:09+00:00"
},
{
"name": "spatie/commonmark-shiki-highlighter",
"version": "2.5.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/commonmark-shiki-highlighter.git",
"reference": "595c7e0b45d4a63b17dfc1ccbd13532d431ec351"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/commonmark-shiki-highlighter/zipball/595c7e0b45d4a63b17dfc1ccbd13532d431ec351",
"reference": "595c7e0b45d4a63b17dfc1ccbd13532d431ec351",
"shasum": ""
},
"require": {
"league/commonmark": "^2.4.2",
"php": "^8.0",
"spatie/shiki-php": "^2.2.2",
"symfony/process": "^5.4|^6.4|^7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.19|^v3.49.0",
"phpunit/phpunit": "^9.5",
"spatie/phpunit-snapshot-assertions": "^4.2.7",
"spatie/ray": "^1.28"
},
"type": "commonmark-extension",
"autoload": {
"psr-4": {
"Spatie\\CommonMarkShikiHighlighter\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Highlight code blocks with league/commonmark and Shiki",
"homepage": "https://github.com/spatie/commonmark-shiki-highlighter",
"keywords": [
"commonmark-shiki-highlighter",
"spatie"
],
"support": {
"source": "https://github.com/spatie/commonmark-shiki-highlighter/tree/2.5.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-01-13T11:25:47+00:00"
},
{
"name": "spatie/laravel-activitylog",
"version": "4.10.2",
@@ -8076,6 +8136,82 @@
],
"time": "2025-06-25T11:36:37+00:00"
},
{
"name": "spatie/laravel-markdown",
"version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-markdown.git",
"reference": "353e7f9fae62826e26cbadef58a12ecf39685280"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-markdown/zipball/353e7f9fae62826e26cbadef58a12ecf39685280",
"reference": "353e7f9fae62826e26cbadef58a12ecf39685280",
"shasum": ""
},
"require": {
"illuminate/cache": "^9.0|^10.0|^11.0|^12.0",
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
"illuminate/view": "^9.0|^10.0|^11.0|^12.0",
"league/commonmark": "^2.6.0",
"php": "^8.1",
"spatie/commonmark-shiki-highlighter": "^2.5",
"spatie/laravel-package-tools": "^1.4.3"
},
"require-dev": {
"brianium/paratest": "^6.2|^7.8",
"nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0",
"orchestra/testbench": "^6.15|^7.0|^8.0|^10.0",
"pestphp/pest": "^1.22|^2.0|^3.7",
"phpunit/phpunit": "^9.3|^11.5.3",
"spatie/laravel-ray": "^1.23",
"spatie/pest-plugin-snapshots": "^1.1|^2.2|^3.0",
"vimeo/psalm": "^4.8|^6.7"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\LaravelMarkdown\\MarkdownServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\LaravelMarkdown\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "A highly configurable markdown renderer and Blade component for Laravel",
"homepage": "https://github.com/spatie/laravel-markdown",
"keywords": [
"Laravel-Markdown",
"laravel",
"spatie"
],
"support": {
"source": "https://github.com/spatie/laravel-markdown/tree/2.7.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-02-21T13:43:18+00:00"
},
{
"name": "spatie/laravel-package-tools",
"version": "1.92.7",
@@ -8515,6 +8651,71 @@
],
"time": "2025-04-18T08:17:40+00:00"
},
{
"name": "spatie/shiki-php",
"version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/shiki-php.git",
"reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/shiki-php/zipball/a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
"reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.0",
"symfony/process": "^5.4|^6.4|^7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^v3.0",
"pestphp/pest": "^1.8",
"phpunit/phpunit": "^9.5",
"spatie/pest-plugin-snapshots": "^1.1",
"spatie/ray": "^1.10"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\ShikiPhp\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rias Van der Veken",
"email": "rias@spatie.be",
"role": "Developer"
},
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Highlight code using Shiki in PHP",
"homepage": "https://github.com/spatie/shiki-php",
"keywords": [
"shiki",
"spatie"
],
"support": {
"source": "https://github.com/spatie/shiki-php/tree/2.3.2"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-02-21T14:16:57+00:00"
},
{
"name": "spatie/url",
"version": "2.4.0",

View File

@@ -22,7 +22,8 @@ return [
'services' => [
// Temporary disabled until cache is implemented
// 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/v4.x/templates/service-templates-latest.json',
'file_name' => 'service-templates-latest.json',
],
'terminal' => [

View File

@@ -65,6 +65,6 @@ return [
'client_secret' => env('ZITADEL_CLIENT_SECRET'),
'redirect' => env('ZITADEL_REDIRECT_URI'),
'base_url' => env('ZITADEL_BASE_URL'),
]
],
];

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_changelog_reads', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('release_tag'); // GitHub tag_name (e.g., "v4.0.0-beta.420.6")
$table->timestamp('read_at');
$table->timestamps();
$table->unique(['user_id', 'release_tag']);
$table->index('user_id');
$table->index('release_tag');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_changelog_reads');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->boolean('disable_local_backup')->default(false)->after('save_s3');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->dropColumn('disable_local_backup');
});
}
};

View File

@@ -120,6 +120,7 @@ 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
COPY --chown=www-data:www-data changelogs/ ./changelogs/
RUN composer dump-autoload

3
public/svgs/bluesky.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="600" height="530" xmlns="http://www.w3.org/2000/svg">
<path d="M135.72 44.03C202.216 93.951 273.74 195.17 300 249.49c26.262-54.316 97.782-155.54 164.28-205.46C512.26 8.009 590-19.862 590 68.825c0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.38-3.69-10.832-3.708-7.896-.017-2.936-1.193.516-3.707 7.896-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.45-163.25-81.433C20.15 217.613 9.997 86.535 9.997 68.825c0-88.687 77.742-60.816 125.72-24.795z" fill="#1185fe"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B

BIN
public/svgs/drizzle.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

11
public/svgs/homebox.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg viewBox="0 0 10817 9730" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:5.42683">
<path d="M9310.16 2560.9c245.302 249.894 419.711 539.916 565.373 845.231 47.039 98.872 36.229 215.514-28.2 304.05-64.391 88.536-172.099 134.676-280.631 120.28 0 .053-.039.053-.039.053" style="fill:gray;stroke:#000;stroke-width:206.41px"/>
<path d="M5401.56 487.044c-127.958 6.227-254.855 40.77-370.992 103.628-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43s-249.048 416.428-470.593 786.926c-189.24 316.445-592.833 429.831-919.198 258.219l-2699.36-1419.32v2215.59c0 226.273 128.751 435.33 337.755 548.466 764.649 413.885 2620.97 1418.66 3385.59 1832.51 209.018 113.137 466.496 113.137 675.514 0 764.623-413.857 2620.94-1418.63 3385.59-1832.51 208.989-113.136 337.743-322.193 337.743-548.466v-3513.48c0-318.684-174.59-611.722-454.853-763.409-795.543-430.632-2427.75-1314.09-3193.02-1728.32-141.693-76.684-299.364-111.227-455.442-103.628" style="fill:#dadada;stroke:#000;stroke-width:206.42px"/>
<path d="M5471.83 4754.46V504.71c-127.958 6.226-325.127 23.1-441.264 85.958-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43Z" style="fill:gray;stroke:#000;stroke-width:206.42px"/>
<path d="m1459.34 2725.96-373.791 715.667c-177.166 339.292-46.417 758 292.375 936.167l4.75 2.5m0 0 2699.37 1419.29c326.374 171.625 729.916 58.25 919.165-258.208 221.542-370.5 470.583-786.917 470.583-786.917l-3963.04-2122.42-2.167 3.458-47.25 90.458" style="fill:#dadada;stroke:#000;stroke-width:206.42px"/>
<path d="M5443.74 520.879v4149.79" style="fill:none;stroke:#000;stroke-width:153.5px"/>
<path d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59Z" style="fill:#567f67"/>
<path d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59ZM6463.98 5551.29v1387.06l2301.77-1328.92V4222.37L6463.98 5551.29Z"/>
<path d="M5443.76 9041.74v-4278.4" style="fill:none;stroke:#000;stroke-width:206.44px;stroke-linejoin:miter"/>
<path d="m5471.79 4773.86 3829.35-2188.22" style="fill:none;stroke:#000;stroke-width:206.43px;stroke-linejoin:miter"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

32
public/svgs/librechat.svg Normal file
View File

@@ -0,0 +1,32 @@
<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient22708">
<stop stop-color="#21facf" offset="0"/>
<stop stop-color="#0970ef" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient6949" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientTransform="translate(-5.754,-56.594)" gradientUnits="userSpaceOnUse">
<stop stop-color="#72004e" offset="0"/>
<stop stop-color="#0015b1" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient22718" x1="56.735" x2="155.2" y1="246.96" y2="58.575" gradientUnits="userSpaceOnUse">
<stop stop-color="#4f00da" offset="0"/>
<stop stop-color="#e5311b" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient23463" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
<linearGradient id="linearGradient903" x1="54.478" x2="192.1" y1="247.56" y2="9.8095" gradientTransform="matrix(.87923 0 0 .87923 -9.551 48.787)" gradientUnits="userSpaceOnUse">
<stop stop-color="#dc180d" offset="0"/>
<stop stop-color="#f96e20" offset=".5"/>
<stop stop-color="#f4ce41" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient918" x1="39.468" x2="154.99" y1="204.22" y2="124.47" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
</defs>
<g transform="matrix(2.473 0 0 2.473 -4.8978 -4.8812)">
<path transform="translate(-5.5496,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" display="none" fill="url(#linearGradient903)"/>
<path transform="translate(-5.5498,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" fill="url(#linearGradient918)"/>
<g transform="translate(0 2.0218e-5)">
<path transform="translate(-5.7543,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient22718)"/>
<path transform="translate(-5.754,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient23463)"/>
<path d="m105.5 24.43c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" fill="url(#linearGradient6949)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -8,133 +8,85 @@
data_get($application, 'previews', collect([]))->count() > 0 ||
data_get($application, 'ports_mappings_array')) &&
data_get($application, 'settings.is_raw_compose_deployment_enabled') !== true)
@if (data_get($application, 'gitBrancLocation'))
<a target="_blank" class="dropdown-item" href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
Git Repository
</a>
@endif
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach (collect(json_decode($this->application->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort($domain) }}
</a>
@endforeach
@endif
@endforeach
@endif
@if (data_get($application, 'fqdn'))
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort($fqdn) }}
<div class="flex flex-col gap-1">
@if (data_get($application, 'gitBrancLocation'))
<a target="_blank" class="dropdown-item" href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
Git Repository
</a>
@endforeach
@endif
@if (data_get($application, 'previews', collect())->count() > 0)
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach ($application->previews as $preview)
@foreach (collect(json_decode($preview->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>PR{{ data_get($preview, 'pull_request_id') }} |
{{ getFqdnWithoutPort($domain) }}
</a>
@endforeach
@endif
@endforeach
@endforeach
@else
@foreach (data_get($application, 'previews') as $preview)
@if (data_get($preview, 'fqdn'))
<a class="dropdown-item" target="_blank"
href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
PR{{ data_get($preview, 'pull_request_id') }} |
{{ data_get($preview, 'fqdn') }}
</a>
@endif
@endforeach
@endif
@endif
@if (data_get($application, 'ports_mappings_array'))
@foreach ($application->ports_mappings_array as $port)
@if ($application->destination->server->id === 0)
<a class="dropdown-item" target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
Port {{ $port }}
</a>
@else
<a class="dropdown-item" target="_blank"
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}
</a>
@if (count($application->additional_servers) > 0)
@foreach ($application->additional_servers as $server)
<a class="dropdown-item" target="_blank"
href="http://{{ $server->ip }}:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
{{ $server->ip }}:{{ explode(':', $port)[0] }}
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach (collect(json_decode($this->application->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
<x-external-link class="size-4" />{{ getFqdnWithoutPort($domain) }}
</a>
@endforeach
@endif
@endforeach
@endif
@if (data_get($application, 'fqdn'))
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<x-external-link class="size-4" />{{ getFqdnWithoutPort($fqdn) }}
</a>
@endforeach
@endif
@if (data_get($application, 'previews', collect())->count() > 0)
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach ($application->previews as $preview)
@foreach (collect(json_decode($preview->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
<a class="dropdown-item" target="_blank" href="{{ getFqdnWithoutPort($domain) }}">
<x-external-link class="size-4" />PR{{ data_get($preview, 'pull_request_id') }}
|
{{ getFqdnWithoutPort($domain) }}
</a>
@endforeach
@endif
@endforeach
@endforeach
@else
@foreach (data_get($application, 'previews') as $preview)
@if (data_get($preview, 'fqdn'))
<a class="dropdown-item" target="_blank"
href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
<x-external-link class="size-4" />
PR{{ data_get($preview, 'pull_request_id') }} |
{{ data_get($preview, 'fqdn') }}
</a>
@endif
@endforeach
@endif
@endforeach
@endif
@endif
@if (data_get($application, 'ports_mappings_array'))
@foreach ($application->ports_mappings_array as $port)
@if ($application->destination->server->id === 0)
<a class="dropdown-item" target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
<x-external-link class="size-4" />
Port {{ $port }}
</a>
@else
<a class="dropdown-item" target="_blank"
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
<x-external-link class="size-4" />
{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}
</a>
@if (count($application->additional_servers) > 0)
@foreach ($application->additional_servers as $server)
<a class="dropdown-item" target="_blank"
href="http://{{ $server->ip }}:{{ explode(':', $port)[0] }}">
<x-external-link class="size-4" />
{{ $server->ip }}:{{ explode(':', $port)[0] }}
</a>
@endforeach
@endif
@endif
@endforeach
@endif
</div>
@else
<div class="px-2 py-1.5 text-xs">No links available</div>
@endif

View File

@@ -1,7 +1,6 @@
<svg class="inline-flex w-3 h-3 ml-1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"
focusable="false" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none">
</path>
<path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z" fill="currentColor">
</path>
@props(['class' => 'inline-flex w-3 h-3 dark:text-neutral-400 text-black'])
<svg {{ $attributes->merge(['class' => $class]) }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 356 B

View File

@@ -32,7 +32,7 @@
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled"
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
aria-placeholder="{{ $attributes->get('placeholder') }}">
aria-placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif>
</div>
@else
@@ -45,7 +45,7 @@
max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
maxlength="{{ $attributes->get('maxlength') }}"
@if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}"
placeholder="{{ $attributes->get('placeholder') }}">
placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif>
@endif
@if (!$label && $helper)
<x-helper :helper="$helper" />

View File

@@ -1,119 +1,88 @@
<nav class="flex flex-col flex-1 px-2 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{
switchWidth() {
if (this.full === 'full') {
localStorage.setItem('pageWidth', 'center');
} else {
localStorage.setItem('pageWidth', 'full');
}
window.location.reload();
},
setZoom(zoom) {
localStorage.setItem('zoom', zoom);
window.location.reload();
},
init() {
this.full = localStorage.getItem('pageWidth');
this.zoom = localStorage.getItem('zoom');
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
const userSettings = localStorage.getItem('theme');
if (userSettings !== 'system') {
return;
}
if (e.matches) {
document.documentElement.classList.add('dark');
<nav class="flex flex-col flex-1 px-2 bg-white border-r dark:border-coolgray-200 border-neutral-300 dark:bg-base"
x-data="{
switchWidth() {
if (this.full === 'full') {
localStorage.setItem('pageWidth', 'center');
} else {
localStorage.setItem('pageWidth', 'full');
}
window.location.reload();
},
setZoom(zoom) {
localStorage.setItem('zoom', zoom);
window.location.reload();
},
init() {
this.full = localStorage.getItem('pageWidth');
this.zoom = localStorage.getItem('zoom');
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
const userSettings = localStorage.getItem('theme');
if (userSettings !== 'system') {
return;
}
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
});
this.queryTheme();
this.checkZoom();
},
setTheme(type) {
this.theme = type;
localStorage.setItem('theme', type);
this.queryTheme();
},
queryTheme() {
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
const userSettings = localStorage.getItem('theme') || 'dark';
localStorage.setItem('theme', userSettings);
if (userSettings === 'dark') {
document.documentElement.classList.add('dark');
this.theme = 'dark';
} else if (userSettings === 'light') {
document.documentElement.classList.remove('dark');
this.theme = 'light';
} else if (darkModePreference) {
this.theme = 'system';
document.documentElement.classList.add('dark');
} else if (!darkModePreference) {
this.theme = 'system';
document.documentElement.classList.remove('dark');
}
});
this.queryTheme();
this.checkZoom();
},
setTheme(type) {
this.theme = type;
localStorage.setItem('theme', type);
this.queryTheme();
},
queryTheme() {
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
const userSettings = localStorage.getItem('theme') || 'dark';
localStorage.setItem('theme', userSettings);
if (userSettings === 'dark') {
document.documentElement.classList.add('dark');
this.theme = 'dark';
} else if (userSettings === 'light') {
document.documentElement.classList.remove('dark');
this.theme = 'light';
} else if (darkModePreference) {
this.theme = 'system';
document.documentElement.classList.add('dark');
} else if (!darkModePreference) {
this.theme = 'system';
document.documentElement.classList.remove('dark');
}
},
checkZoom() {
if (this.zoom === null) {
this.setZoom(100);
}
if (this.zoom === '90') {
const style = document.createElement('style');
style.textContent = `
html {
font-size: 93.75%;
}
:root {
--vh: 1vh;
}
@media (min-width: 1024px) {
},
checkZoom() {
if (this.zoom === null) {
this.setZoom(100);
}
if (this.zoom === '90') {
const style = document.createElement('style');
style.textContent = `
html {
font-size: 87.5%;
font-size: 93.75%;
}
}
`;
document.head.appendChild(style);
:root {
--vh: 1vh;
}
@media (min-width: 1024px) {
html {
font-size: 87.5%;
}
}
`;
document.head.appendChild(style);
}
}
}
}">
}">
<div class="flex pt-6 pb-4 pl-2">
<div class="flex flex-col w-full">
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
<x-version />
</div>
<div class="pt-1">
<x-dropdown>
<x-slot:title>
<div class="flex justify-end w-8" x-show="theme === 'dark' || theme === 'system'">
<svg class="w-5 h-5 rounded-sm dark:fill-white" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
</div>
<div class="flex justify-end w-8" x-show="theme === 'light'">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</div>
</x-slot:title>
<div class="flex flex-col gap-1">
<div class="font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Color</div>
<button @click="setTheme('dark')" class="px-1 dropdown-item-no-padding">Dark</button>
<button @click="setTheme('light')" class="px-1 dropdown-item-no-padding">Light</button>
<button @click="setTheme('system')" class="px-1 dropdown-item-no-padding">System</button>
<div class="my-1 font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Width</div>
<button @click="switchWidth()" class="px-1 dropdown-item-no-padding"
x-show="full === 'full'">Center</button>
<button @click="switchWidth()" class="px-1 dropdown-item-no-padding"
x-show="full === 'center'">Full</button>
<div class="my-1 font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Zoom</div>
<button @click="setZoom(100)" class="px-1 dropdown-item-no-padding">100%</button>
<button @click="setZoom(90)" class="px-1 dropdown-item-no-padding">90%</button>
</div>
</x-dropdown>
</div>
<livewire:settings-dropdown />
</div>
<div class="px-2 pt-2 pb-7">
<livewire:switch-team />
@@ -196,8 +165,8 @@
class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('storage.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
@@ -211,8 +180,8 @@
class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('shared-variables.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path
d="M5 4C2.5 9 2.5 14 5 20M19 4c2.5 5 2.5 10 0 16M9 9h1c1 0 1 1 2.016 3.527C13 15 13 16 14 16h1" />
<path d="M8 16c1.5 0 3-2 4-3.5S14.5 9 16 9" />

View File

@@ -5,13 +5,7 @@
</x-slot>
@foreach ($links as $link)
<a class="dropdown-item" target="_blank" href="{{ $link }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ $link }}
<x-external-link class="size-4" />{{ $link }}
</a>
@endforeach
</x-dropdown>

View File

@@ -6,7 +6,8 @@
@if ($exception->getMessage())
<p class="text-base leading-7 text-red-500">{{ $exception->getMessage() }}</p>
@else
<p class="text-base leading-7 text-neutral-300">The request could not be understood by the server due to
<p class="text-base leading-7 dark:text-neutral-400 text-black">The request could not be understood by the
server due to
malformed syntax.
</p>
@endif

View File

@@ -3,7 +3,7 @@
<div>
<p class="font-mono font-semibold text-7xl dark:text-warning">401</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">You shall not pass!</h1>
<p class="text-base leading-7 text-neutral-300">You don't have permission to access this page.
<p class="text-base leading-7 dark:text-neutral-400 text-black">You don't have permission to access this page.
</p>
<div class="flex items-center mt-10 gap-x-6">
<a href="/">

View File

@@ -3,7 +3,7 @@
<div>
<p class="font-mono font-semibold text-7xl dark:text-warning">403</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">You shall not pass!</h1>
<p class="text-base leading-7 text-neutral-300">You don't have permission to access this page.
<p class="text-base leading-7 dark:text-neutral-400 text-black">You don't have permission to access this page.
</p>
<div class="flex items-center mt-10 gap-x-6">
<a href="/">

View File

@@ -3,7 +3,7 @@
<div>
<p class="font-mono font-semibold text-7xl dark:text-warning">404</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">How did you get here?</h1>
<p class="text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
<p class="text-base leading-7 dark:text-neutral-400 text-black">Sorry, we couldnt find the page youre looking
for.
</p>
<div class="flex items-center mt-10 gap-x-6">

View File

@@ -3,7 +3,7 @@
<div>
<p class="font-mono font-semibold text-7xl dark:text-warning">419</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">This page is definitely old, not like you!</h1>
<p class="text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
<p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldnt find the page youre looking
for.
</p>
<div class="flex items-center mt-10 gap-x-6">

View File

@@ -3,7 +3,8 @@
<div>
<p class="font-mono font-semibold text-7xl dark:text-warning">429</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">Woah, slow down there!</h1>
<p class="text-base leading-7 text-neutral-300">You're making too many requests. Please wait a few
<p class="text-base leading-7 dark:text-neutral-400 text-black">You're making too many requests. Please wait a
few
seconds before trying again.
</p>
<div class="flex items-center mt-10 gap-x-6">

View File

@@ -3,7 +3,8 @@
<div class="w-full max-w-3xl px-8">
<p class="font-mono font-semibold text-red-500 text-[200px] leading-none">500</p>
<h1 class="text-3xl font-bold tracking-tight dark:text-white">Wait, this is not cool...</h1>
<p class="mt-2 text-lg leading-7 text-neutral-300">There has been an error with the following error message:</p>
<p class="mt-2 text-lg leading-7 dark:text-neutral-400 text-black">There has been an error with the following
error message:</p>
@if ($exception->getMessage() !== '')
<div class="mt-6 text-sm text-red-500">
{!! Purify::clean($exception->getMessage()) !!}
@@ -13,8 +14,8 @@
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-sm hover:text-neutral-300 flex items-center gap-1" href="{{ config('constants.urls.contact') }}">
Contact support
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support
<x-external-link />
</a>
</div>

View File

@@ -3,14 +3,13 @@
<div>
<p class="font-mono font-semibold text-7xl dark:text-warning">503</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">We are working on serious things.</h1>
<p class="text-base leading-7 text-black dark:text-neutral-300">Service Unavailable. Be right back. Thanks for your
<p class="text-base leading-7 dark:text-neutral-400 text-black">Service Unavailable. Be right back. Thanks for
your
patience.
</p>
<div class="flex items-center mt-10 gap-x-6">
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support
<x-external-link />
</a>
</div>
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support
<x-external-link />
</a>
</div>
</div>

View File

@@ -66,7 +66,8 @@
<h3 class="py-4">Deployments</h3>
<div class="flex flex-wrap w-full gap-4">
@foreach (data_get($application, 'previews') as $previewName => $preview)
<div class="flex flex-col w-full p-4 border dark:border-coolgray-200" wire:key="preview-container-{{ $preview->pull_request_id }}">
<div class="flex flex-col w-full p-4 border dark:border-coolgray-200"
wire:key="preview-container-{{ $preview->pull_request_id }}">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (str(data_get($preview, 'status'))->startsWith('running'))
<x-status.running :status="data_get($preview, 'status')" />
@@ -85,6 +86,18 @@
PR on Git
<x-external-link />
</a>
@if (count($parameters) > 0)
|
<a
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Deployment Logs
</a>
|
<a
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Application Logs
</a>
@endif
</div>
@if ($application->build_pack === 'dockercompose')
@@ -100,7 +113,8 @@
</form>
@else
@foreach (collect(json_decode($preview->docker_compose_domains)) as $serviceName => $service)
<livewire:project.application.previews-compose wire:key="preview-{{ $preview->pull_request_id }}-{{ $serviceName }}"
<livewire:project.application.previews-compose
wire:key="preview-{{ $preview->pull_request_id }}-{{ $serviceName }}"
:service="$service" :serviceName="$serviceName" :preview="$preview" />
@endforeach
@endif
@@ -114,21 +128,7 @@
Domain</x-forms.button>
</form>
@endif
<div class="flex items-center gap-2 pt-6">
@if (count($parameters) > 0)
<a
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button>
Deployment Logs
</x-forms.button>
</a>
<a
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button>
Application Logs
</x-forms.button>
</a>
@endif
<div class="flex flex-col xl:flex-row xl:items-center gap-2 pt-6">
<div class="flex-1"></div>
<x-forms.button
wire:click="force_deploy_without_cache({{ data_get($preview, 'pull_request_id') }})">

View File

@@ -18,7 +18,7 @@
shortConfirmationLabel="Database Name" />
@endif
</div>
<div class="w-48 pb-2">
<div class="w-64 pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
@if ($s3s->count() > 0)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
@@ -26,11 +26,18 @@
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
disabled />
@endif
@if ($backup->save_s3)
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@else
<x-forms.checkbox disabled label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@endif
</div>
@if ($backup->save_s3)
<div class="pb-6">
<x-forms.select id="s3StorageId" label="S3 Storage" required>
<option value="default">Select a S3 storage</option>
<option value="default" disabled>Select a S3 storage</option>
@foreach ($s3s as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
@@ -77,7 +84,7 @@
<x-forms.input label="Frequency" id="frequency" />
<x-forms.input label="Timezone" id="timezone" disabled
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" />
<x-forms.input label="Timeout" id="timeout" helper="The timeout of the backup job in seconds."/>
<x-forms.input label="Timeout" id="timeout" helper="The timeout of the backup job in seconds." />
</div>
<h3 class="mt-6 mb-2 text-lg font-medium">Backup Retention Settings</h3>

View File

@@ -132,8 +132,16 @@
id="database.public_port" label="Public Port" />
</div>
<x-forms.textarea
helper="<a target='_blank' class='underline dark:text-white' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>Redis Default Configuration</a>"
label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
placeholder="# maxmemory 256mb
# maxmemory-policy allkeys-lru
# timeout 300"
helper="You only need to provide the Redis directives you want to override — Redis will use default values for everything else. <br/><br/>
⚠️ <strong>Important:</strong> Coolify automatically applies the requirepass directive using the password shown in the Password field above. If you override requirepass in your custom configuration, make sure it matches the password field to avoid authentication issues. <br/><br/>
🔗 <strong>Tip:</strong> <a target='_blank' class='underline dark:text-white' href='https://raw.githubusercontent.com/redis/redis/7.2/redis.conf'>View the full Redis default configuration</a> to see what options are available."
label="Custom Redis Configuration" rows="10" id="database.redis_conf" />
<h3 class="pt-4">Advanced</h3>
<div class="flex flex-col">
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."

View File

@@ -1,84 +1,85 @@
<div>
<div x-data x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
<h1>Create a new Application</h1>
<div class="pb-4">Deploy any public Git repositories.</div>
<!-- Repository URL Form -->
<form class="flex flex-col gap-2" wire:submit='loadBranch'>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-end">
<x-forms.input required id="repository_url" label="Repository URL (https://)"
helper="{!! __('repository.url') !!}" />
<x-forms.button type="submit">
Check repository
</x-forms.button>
</div>
<div>
For example application deployments, checkout <a class="underline dark:text-white"
href="https://github.com/coollabsio/coolify-examples/" target="_blank">Coolify
Examples</a>.
</div>
@if ($branchFound)
@if ($rate_limit_remaining && $rate_limit_reset)
<div class="flex gap-2 py-2">
<div>Rate Limit</div>
<x-helper
helper="Rate limit remaining: {{ $rate_limit_remaining }}<br>Rate limit reset at: {{ $rate_limit_reset }} UTC" />
</div>
@endif
<div class="flex flex-col gap-2 pb-6">
<div class="flex gap-2">
@if ($git_source === 'other')
<x-forms.input id="git_branch" label="Branch"
helper="You can select other branches after configuration is done." />
@else
<x-forms.input disabled id="git_branch" label="Branch"
helper="You can select other branches after configuration is done." />
@endif
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option>
<option value="dockercompose">Docker Compose</option>
</x-forms.select>
@if ($isStatic)
<x-forms.input id="publish_directory" label="Publish Directory"
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
@endif
</div>
@if ($build_pack === 'dockercompose')
<x-forms.input placeholder="/" wire:model.blur-sm="base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur-sm="docker_compose_location"
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($base_directory . $docker_compose_location, '/') }}</span>" />
Compose file location in your repository:<span
class='dark:text-warning'>{{ Str::start($base_directory . $docker_compose_location, '/') }}</span>
@else
<x-forms.input wire:model="base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@endif
@if ($show_is_static)
<x-forms.input type="number" id="port" label="Port" :readonly="$isStatic || $build_pack === 'static'"
helper="The port your application listens on." />
<div class="w-64">
<x-forms.checkbox instantSave id="isStatic" label="Is it a static site?"
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
</div>
@endif
{{-- <div class="w-64">
<x-forms.checkbox helper="If your repository contains a coolify.json file, it will be used to configure your application." instantSave id="checkCoolifyConfig" label="Use coolify.json if exists?" />
</div> --}}
{{-- @if ($build_pack === 'dockercompose' && isDev())
<div class="dark:text-warning">If you choose Docker Compose based deployments, you cannot
change it afterwards.</div>
<x-forms.checkbox instantSave label="New Compose Services (only in dev mode)"
id="new_compose_services"></x-forms.checkbox>
@endif --}}
</div>
<x-forms.button wire:click.prevent='submit'>
Continue
</x-forms.button>
@endif
<div class="flex gap-2 items-end">
<x-forms.input required id="repository_url" label="Repository URL (https://)"
helper="{!! __('repository.url') !!}" autofocus />
<x-forms.button type="submit">
Check repository
</x-forms.button>
</div>
<div>
For example application deployments, checkout <a class="underline dark:text-white"
href="https://github.com/coollabsio/coolify-examples/" target="_blank">Coolify
Examples</a>.
</div>
</div>
</form>
@if ($branchFound)
@if ($rate_limit_remaining && $rate_limit_reset)
<div class="flex gap-2 py-2">
<div>Rate Limit</div>
<x-helper
helper="Rate limit remaining: {{ $rate_limit_remaining }}<br>Rate limit reset at: {{ $rate_limit_reset }} UTC" />
</div>
@endif
<!-- Application Configuration Form -->
<form class="flex flex-col gap-2 pt-4" wire:submit='submit'>
<div class="flex flex-col gap-2 pb-6">
<div class="flex gap-2">
@if ($git_source === 'other')
<x-forms.input id="git_branch" label="Branch"
helper="You can select other branches after configuration is done." />
@else
<x-forms.input disabled id="git_branch" label="Branch"
helper="You can select other branches after configuration is done." />
@endif
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option>
<option value="dockercompose">Docker Compose</option>
</x-forms.select>
@if ($isStatic)
<x-forms.input id="publish_directory" label="Publish Directory"
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
@endif
</div>
@if ($build_pack === 'dockercompose')
<div x-data="{ baseDir: '{{ $base_directory }}', composeLocation: '{{ $docker_compose_location }}' }" class="gap-2 flex flex-col">
<x-forms.input placeholder="/" wire:model.blur-sm="base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." x-model="baseDir" />
<x-forms.input placeholder="/docker-compose.yaml" wire:model.blur-sm="docker_compose_location"
label="Docker Compose Location" helper="It is calculated together with the Base Directory."
x-model="composeLocation" />
<div class="pt-2">
<span>
Compose file location in your repository: </span><span class='dark:text-warning'
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></span>
</div>
</div>
@else
<x-forms.input wire:model="base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@endif
@if ($show_is_static)
<x-forms.input type="number" id="port" label="Port" :readonly="$isStatic || $build_pack === 'static'"
helper="The port your application listens on." />
<div class="w-64">
<x-forms.checkbox instantSave id="isStatic" label="Is it a static site?"
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
</div>
@endif
</div>
<x-forms.button type="submit">
Continue
</x-forms.button>
</form>
@endif
</div>

View File

@@ -2,6 +2,9 @@
<div>
<div class="flex gap-2">
<h2>Service Stack</h2>
@if (isDev())
<div>{{ $service->compose_parsing_version }}</div>
@endif
<x-forms.button wire:target='submit' type="submit">Save</x-forms.button>
<x-modal-input buttonTitle="Edit Compose File" title="Edit Docker Compose" :closeOutside="false">
<livewire:project.service.edit-compose serviceId="{{ $service->id }}" />
@@ -25,12 +28,12 @@
@foreach ($fields as $serviceName => $field)
<div class="flex items-center gap-2"><span
class="font-bold">{{ data_get($field, 'serviceName') }}</span>{{ data_get($field, 'name') }}
@if (data_get($field, 'customHelper'))
<x-helper helper="{{ data_get($field, 'customHelper') }}" />
@else
<x-helper helper="Variable name: {{ $serviceName }}" />
@endif
</div>
@if (data_get($field, 'customHelper'))
<x-helper helper="{{ data_get($field, 'customHelper') }}" />
@else
<x-helper helper="Variable name: {{ $serviceName }}" />
@endif
</div>
<x-forms.input type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
required="{{ str(data_get($field, 'rules'))?->contains('required') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>

View File

@@ -9,7 +9,8 @@
</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
<p class="mt-2 text-sm text-neutral-300">No shell (bash/sh) is available in this container.
Please
ensure either bash or sh is installed to use the terminal.</p>
</div>
</div>

View File

@@ -22,6 +22,10 @@
soon.<br/>Status notifications sent every week.<br/>You can disable notifications in the <a class='dark:text-white underline' href='{{ route('notifications.email') }}'>notification settings</a>." />
<x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')">
Check Now</x-forms.button>
@if (isDev())
<x-forms.button type="button" wire:click="sendTestEmail">
Send Test Email (dev only)</x-forms.button>
@endif
</div>
<div>Update your servers semi-automatically.</div>
<div>

View File

@@ -0,0 +1,337 @@
<div x-data="{
dropdownOpen: false,
search: '',
allEntries: [],
init() {
this.mounted();
// Load all entries when component initializes
this.allEntries = @js($entries->toArray());
},
markEntryAsRead(tagName) {
// Update the entry in our local Alpine data
const entry = this.allEntries.find(e => e.tag_name === tagName);
if (entry) {
entry.is_read = true;
}
// Call Livewire to update server-side
$wire.markAsRead(tagName);
},
markAllEntriesAsRead() {
// Update all entries in our local Alpine data
this.allEntries.forEach(entry => {
entry.is_read = true;
});
// Call Livewire to update server-side
$wire.markAllAsRead();
},
switchWidth() {
if (this.full === 'full') {
localStorage.setItem('pageWidth', 'center');
} else {
localStorage.setItem('pageWidth', 'full');
}
window.location.reload();
},
setZoom(zoom) {
localStorage.setItem('zoom', zoom);
window.location.reload();
},
setTheme(type) {
this.theme = type;
localStorage.setItem('theme', type);
this.queryTheme();
},
queryTheme() {
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
const userSettings = localStorage.getItem('theme') || 'dark';
localStorage.setItem('theme', userSettings);
if (userSettings === 'dark') {
document.documentElement.classList.add('dark');
this.theme = 'dark';
} else if (userSettings === 'light') {
document.documentElement.classList.remove('dark');
this.theme = 'light';
} else if (darkModePreference) {
this.theme = 'system';
document.documentElement.classList.add('dark');
} else if (!darkModePreference) {
this.theme = 'system';
document.documentElement.classList.remove('dark');
}
},
mounted() {
this.full = localStorage.getItem('pageWidth');
this.zoom = localStorage.getItem('zoom');
this.queryTheme();
},
get filteredEntries() {
let entries = this.allEntries;
// Apply search filter if search term exists
if (this.search && this.search.trim() !== '') {
const searchLower = this.search.trim().toLowerCase();
entries = entries.filter(entry => {
return (entry.title?.toLowerCase().includes(searchLower) ||
entry.content?.toLowerCase().includes(searchLower) ||
entry.tag_name?.toLowerCase().includes(searchLower));
});
}
// Always sort: unread first, then by published date (newest first)
return entries.sort((a, b) => {
// First sort by read status (unread first)
if (a.is_read !== b.is_read) {
return a.is_read ? 1 : -1; // unread (false) comes before read (true)
}
// Then sort by published date (newest first)
return new Date(b.published_at) - new Date(a.published_at);
});
}
}" @click.outside="dropdownOpen = false">
<!-- Custom Dropdown without arrow -->
<div class="relative">
<button @click="dropdownOpen = !dropdownOpen"
class="relative p-2 dark:text-neutral-400 hover:dark:text-white transition-colors cursor-pointer"
title="Settings">
<!-- Gear Icon -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Settings">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<!-- Unread Count Badge -->
@if ($unreadCount > 0)
<span
class="absolute -top-1 -right-1 bg-error text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
</span>
@endif
</button>
<!-- Dropdown Menu -->
<div x-show="dropdownOpen" x-transition:enter="ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="ease-in duration-150" x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-2" class="absolute right-0 top-full mt-1 z-50 w-48" x-cloak>
<div
class="p-1 bg-white border rounded-sm shadow-lg dark:bg-coolgray-200 dark:border-black border-neutral-300">
<div class="flex flex-col gap-1">
<!-- What's New Section -->
@if ($unreadCount > 0)
<button wire:click="openWhatsNewModal" @click="dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center justify-between">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>What's New</span>
</div>
<span
class="bg-error text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
</span>
</button>
@else
<button wire:click="openWhatsNewModal" @click="dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>Changelog</span>
</button>
@endif
<!-- Divider -->
<div class="border-b dark:border-coolgray-500 border-neutral-300"></div>
<!-- Theme Section -->
<div class="font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white pb-1">
Appearance</div>
<button @click="setTheme('dark'); dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<span>Dark</span>
</button>
<button @click="setTheme('light'); dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<span>Light</span>
</button>
<button @click="setTheme('system'); dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span>System</span>
</button>
<!-- Width Section -->
<div
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
Width</div>
<button @click="switchWidth(); dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'full'">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h7" />
</svg>
<span>Center</span>
</button>
<button @click="switchWidth(); dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2" x-show="full === 'center'">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16" />
</svg>
<span>Full</span>
</button>
<!-- Zoom Section -->
<div
class="my-1 font-bold border-b dark:border-coolgray-500 border-neutral-300 dark:text-white text-md">
Zoom</div>
<button @click="setZoom(100); dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<span>100%</span>
</button>
<button @click="setZoom(90); dropdownOpen = false"
class="px-1 dropdown-item-no-padding flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 10h4v4h-4v-4z" />
</svg>
<span>90%</span>
</button>
</div>
</div>
</div>
</div>
<!-- What's New Modal -->
@if ($showWhatsNewModal)
<div class="fixed inset-0 z-50 flex items-center justify-center py-6 px-4"
@keydown.escape.window="$wire.closeWhatsNewModal()">
<!-- Background overlay -->
<div class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs" wire:click="closeWhatsNewModal">
</div>
<!-- Modal panel -->
<div
class="relative w-full h-full max-w-7xl py-6 border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300 flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between pb-3">
<div>
<h3 class="text-2xl font-bold dark:text-white">
Changelog
</h3>
<p class="mt-1 text-sm dark:text-neutral-400">
Stay up to date with the latest features and improvements.
</p>
</div>
<div class="flex items-center gap-2">
@if (isDev())
<x-forms.button wire:click="manualFetchChangelog"
class="bg-coolgray-200 hover:bg-coolgray-300">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Fetch Latest
</x-forms.button>
@endif
@if ($unreadCount > 0)
<x-forms.button @click="markAllEntriesAsRead">
Mark all as read
</x-forms.button>
@endif
<button wire:click="closeWhatsNewModal"
class="flex items-center justify-center w-8 h-8 rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300 outline-0 cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- Search -->
<div class="pb-4 border-b dark:border-coolgray-200 flex-shrink-0">
<div class="relative">
<input x-model="search" placeholder="Search updates..." class="input pl-10" />
<svg class="absolute left-3 top-2 w-4 h-4 dark:text-neutral-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
<!-- Content -->
<div class="py-4 flex-1 overflow-y-auto scrollbar">
<div x-show="filteredEntries.length > 0">
<div class="space-y-4">
<template x-for="entry in filteredEntries" :key="entry.tag_name">
<div class="relative p-4 border dark:border-coolgray-300 rounded-sm"
:class="!entry.is_read ? 'dark:bg-coolgray-200 border-warning' : 'dark:bg-coolgray-100'">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span x-show="entry.title"
class="px-2 py-1 text-xs font-semibold dark:bg-coolgray-300 dark:text-neutral-200 rounded-sm"><a
:href="`https://github.com/coollabsio/coolify/releases/tag/${entry.tag_name}`"
target="_blank"
class="inline-flex items-center gap-1 hover:text-coolgray-500">
<span x-text="entry.title"></span>
<x-external-link />
</a></span>
<span class="text-xs dark:text-neutral-400"
x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span>
</div>
<div class="dark:text-neutral-300 leading-relaxed max-w-none"
x-html="entry.content_html">
</div>
</div>
<button x-show="!entry.is_read" @click="markEntryAsRead(entry.tag_name)"
class="ml-4 px-3 py-1 text-xs dark:text-neutral-400 hover:dark:text-white border dark:border-neutral-600 rounded hover:dark:bg-neutral-700 transition-colors cursor-pointer"
title="Mark as read">
mark as read
</button>
</div>
</div>
</template>
</div>
</div>
<div x-show="filteredEntries.length === 0" class="text-center py-8">
<svg class="mx-auto h-12 w-12 dark:text-neutral-400" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 class="mt-2 text-sm font-medium dark:text-white">No updates found</h3>
<p class="mt-1 text-sm dark:text-neutral-400">
<span x-show="search.trim() !== ''">No updates match your search criteria.</span>
<span x-show="search.trim() === ''">There are no updates available at the moment.</span>
</p>
</div>
</div>
</div>
</div>
@endif
</div>

View File

@@ -45,7 +45,10 @@ Route::group([
Route::get('/projects', [ProjectController::class, 'projects'])->middleware(['api.ability:read']);
Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid'])->middleware(['api.ability:read']);
Route::get('/projects/{uuid}/environments', [ProjectController::class, 'get_environments'])->middleware(['api.ability:read']);
Route::get('/projects/{uuid}/{environment_name_or_uuid}', [ProjectController::class, 'environment_details'])->middleware(['api.ability:read']);
Route::post('/projects/{uuid}/environments', [ProjectController::class, 'create_environment'])->middleware(['api.ability:write']);
Route::delete('/projects/{uuid}/environments/{environment_name_or_uuid}', [ProjectController::class, 'delete_environment'])->middleware(['api.ability:write']);
Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:read']);
Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['api.ability:write']);

View File

@@ -8,13 +8,13 @@ services:
activepieces:
image: "ghcr.io/activepieces/activepieces:latest"
environment:
- SERVICE_FQDN_ACTIVEPIECES
- SERVICE_URL_ACTIVEPIECES
- AP_API_KEY=$SERVICE_PASSWORD_64_APIKEY
- AP_ENCRYPTION_KEY=$SERVICE_PASSWORD_ENCRYPTIONKEY
- AP_ENGINE_EXECUTABLE_PATH=${AP_ENGINE_EXECUTABLE_PATH:-dist/packages/engine/main.js}
- AP_ENVIRONMENT=${AP_ENVIRONMENT:-prod}
- AP_EXECUTION_MODE=${AP_EXECUTION_MODE:-UNSANDBOXED}
- AP_FRONTEND_URL=${SERVICE_FQDN_ACTIVEPIECES}
- AP_FRONTEND_URL=${SERVICE_URL_ACTIVEPIECES}
- AP_JWT_SECRET=$SERVICE_PASSWORD_64_JWT
- AP_POSTGRES_DATABASE=${POSTGRES_DB:-activepieces}
- AP_POSTGRES_HOST=${POSTGRES_HOST:-postgres}

View File

@@ -8,7 +8,7 @@ services:
actual_server:
image: actualbudget/actual-server:latest
environment:
- SERVICE_FQDN_ACTUAL_5006
- SERVICE_URL_ACTUAL_5006
- ACTUAL_LOGIN_METHOD=password
volumes:
- actual_data:/data

View File

@@ -20,13 +20,13 @@ services:
options:
max-size: 1000m
environment:
- SERVICE_FQDN_AFFINE_3010
- SERVICE_URL_AFFINE_3010
- AFFINE_CONFIG_PATH=/root/.affine/config
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-affine}
- NODE_ENV=production
- AFFINE_SERVER_HOST=$SERVICE_FQDN_AFFINE
- AFFINE_SERVER_EXTERNAL_URL=$SERVICE_FQDN_AFFINE
- AFFINE_SERVER_HOST=$SERVICE_URL_AFFINE
- AFFINE_SERVER_EXTERNAL_URL=$SERVICE_URL_AFFINE
- MAILER_HOST=${MAILER_HOST}
- MAILER_PORT=${MAILER_PORT}
- MAILER_USER=${MAILER_USER}

View File

@@ -8,7 +8,7 @@ services:
anything-llm:
image: mintplexlabs/anythingllm
environment:
- SERVICE_FQDN_ANYTHINGLLM_3001
- SERVICE_URL_ANYTHINGLLM_3001
- STORAGE_DIR=/app/server/storage
- DISABLE_TELEMETRY=${DISABLE_TELEMETRY:-true}
- PASSWORDLOWERCASE=${PASSWORDLOWERCASE:-1}

View File

@@ -8,7 +8,7 @@ services:
apprise-api:
image: linuxserver/apprise-api:latest
environment:
- SERVICE_FQDN_APPRISE_8000
- SERVICE_URL_APPRISE_8000
- PUID=1000
- PGID=1000
- TZ=UTC

View File

@@ -7,7 +7,7 @@ services:
appsmith:
image: index.docker.io/appsmith/appsmith-ce:latest
environment:
- SERVICE_FQDN_APPSMITH
- SERVICE_URL_APPSMITH
- APPSMITH_MAIL_ENABLED=${APPSMITH_MAIL_ENABLED:-false}
- APPSMITH_DISABLE_TELEMETRY=${APPSMITH_DISABLE_TELEMETRY:-false}
- APPSMITH_DISABLE_INTERCOM=${APPSMITH_DISABLE_INTERCOM:-true}

View File

@@ -20,7 +20,7 @@ services:
- appwrite-mariadb
- appwrite-redis
environment:
- SERVICE_FQDN_APPWRITE=/
- SERVICE_URL_APPWRITE=/
- _APP_ENV=${_APP_ENV:-production}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
- _APP_LOCALE=${_APP_LOCALE:-en}
@@ -128,7 +128,7 @@ services:
image: appwrite/console:6.0.13
container_name: appwrite-console
environment:
- SERVICE_FQDN_APPWRITE=/console
- SERVICE_URL_APPWRITE=/console
appwrite-realtime:
image: appwrite/appwrite:1.7.4
@@ -138,7 +138,7 @@ services:
- appwrite-mariadb
- appwrite-redis
environment:
- SERVICE_FQDN_APPWRITE=/v1/realtime
- SERVICE_URL_APPWRITE=/v1/realtime
- _APP_ENV=${_APP_ENV:-production}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
- _APP_OPTIONS_ABUSE=${_APP_OPTIONS_ABUSE:-enabled}

View File

@@ -8,7 +8,7 @@ services:
argilla:
image: "argilla/argilla-server:v2.2.0"
environment:
- SERVICE_FQDN_ARGILLA_6900
- SERVICE_URL_ARGILLA_6900
- ARGILLA_HOME_PATH=/var/lib/argilla
- ARGILLA_ELASTICSEARCH=http://elasticsearch:9200
- ARGILLA_DATABASE_URL=postgresql+asyncpg://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB}

View File

@@ -8,7 +8,7 @@ services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:latest
environment:
- SERVICE_FQDN_AUDIOBOOKSHELF_80
- SERVICE_URL_AUDIOBOOKSHELF_80
- TZ=${TIMEZONE:-America/Toronto}
volumes:
- audiobookshelf-audiobooks:/audiobooks

View File

@@ -10,7 +10,7 @@ services:
restart: unless-stopped
command: server
environment:
- SERVICE_FQDN_AUTHENTIKSERVER_9000
- SERVICE_URL_AUTHENTIKSERVER_9000
- AUTHENTIK_REDIS__HOST=${REDIS_HOST:-redis}
- AUTHENTIK_POSTGRESQL__HOST=${POSTGRES_HOST:-postgresql}
- AUTHENTIK_POSTGRESQL__USER=${SERVICE_USER_POSTGRESQL}

View File

@@ -54,8 +54,8 @@ services:
image: bytemark/smtp:latest
platform: linux/amd64
environment:
- SERVICE_FQDN_SMTP
- RELAY_HOST=$SERVICE_FQDN_SMTP
- SERVICE_URL_SMTP
- RELAY_HOST=$SERVICE_URL_SMTP
- RELAY_PORT=${RELAY_PORT:-587}
- RELAY_USERNAME=$SERVICE_EMAIL_SMTP
- RELAY_PASSWORD=$SERVICE_PASSWORD_SMTP
@@ -75,7 +75,7 @@ services:
minio:
condition: service_healthy
environment:
- SERVICE_FQDN_AZIMUTT_4000
- SERVICE_URL_AZIMUTT_4000
- SENTRY=false
- PHX_SERVER=true
- PHX_HOST=$SERVICE_URL_AZIMUTT
@@ -92,7 +92,7 @@ services:
- S3_KEY_ID=${S3_KEY_ID}
- S3_KEY_SECRET=${S3_KEY_SECRET}
- EMAIL_ADAPTER=${EMAIL_ADAPTER:-smtp}
- SMTP_RELAY=$SERVICE_FQDN_SMTP
- SMTP_RELAY=$SERVICE_URL_SMTP
- SMTP_USERNAME=$SERVICE_EMAIL_SMTP
- SMTP_PASSWORD=$SERVICE_PASSWORD_SMTP
- SMTP_PORT=${SMTP_PORT:-587}

View File

@@ -7,11 +7,11 @@ services:
babybuddy:
image: lscr.io/linuxserver/babybuddy:latest
environment:
- SERVICE_FQDN_BABYBUDDY
- SERVICE_URL_BABYBUDDY
- PUID=1000
- PGID=1000
- TZ=Europe/Madrid
- CSRF_TRUSTED_ORIGINS=$SERVICE_FQDN_BABYBUDDY
- CSRF_TRUSTED_ORIGINS=$SERVICE_URL_BABYBUDDY
volumes:
- babybuddy-config:/config
healthcheck:

View File

@@ -10,7 +10,7 @@ services:
beszel:
image: henrygd/beszel:latest
environment:
- SERVICE_FQDN_BESZEL_8090
- SERVICE_URL_BESZEL_8090
volumes:
- beszel_data:/beszel_data

View File

@@ -1,34 +1,64 @@
# ignore: true
# documentation: https://github.com/bluesky-social/pds
# slogan: A social network for the decentralized web
# tags: pds, bluesky, social, network, decentralized
# logo:
# slogan: Bluesky PDS (Personal Data Server)
# tags: bluesky, pds, platform
# logo: svgs/bluesky.svg
# port: 3000
services:
pds:
image: ghcr.io/bluesky-social/pds:0.4
image: 'ghcr.io/bluesky-social/pds:latest'
volumes:
- pds-data:/pds
- ./pds-data:/pds
environment:
- SERVICE_FQDN_PDS_3000
- PDS_JWT_SECRET=${SERVICE_BASE64_PDS}
- PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_PDS}
- PDS_ADMIN_EMAIL=${PDS_ADMIN_EMAIL:-admin@example.com}
- PDS_DATADIR=${PDS_DATADIR:-/pds}
- PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATADIR:-/pds}/blocks
- PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-52428800}
- SERVICE_URL_PDS_3000
- PDS_HOSTNAME=${SERVICE_URL_PDS}
- PDS_DID_PLC_URL=https://plc.directory
- PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
- PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
- PDS_REPORT_SERVICE_URL=https://mod.bsky.app
- PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
- PDS_CRAWLERS=https://bsky.network
- PDS_JWT_SECRET=${SERVICE_PASSWORD_JWT_SECRET}
- PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
- PDS_ADMIN_EMAIL=${SERVICE_EMAIL_ADMIN}
- PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}
- PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}
- PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks
- PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-52428800}
- PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory}
- PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app}
- PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app}
- PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app/xrpc/com.atproto.moderation.createReport}
- PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac}
- PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network}
- LOG_ENABLED=${LOG_ENABLED:-true}
- PDS_EMAIL_SMTP_URL=${PDS_EMAIL_SMTP_URL:-smtp://localhost:8025}
- PDS_EMAIL_FROM_ADDRESS=${PDS_EMAIL_FROM_ADDRESS:-admin@example.com}
- PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_32_ROTATIONKEY}
command: >
sh -c '
echo "Installing curl, bash, and pdsadmin..."
apk add --no-cache curl bash && \
curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh && \
chmod +x /usr/local/bin/pdsadmin.sh && \
ln -sf /usr/local/bin/pdsadmin.sh /usr/local/bin/pdsadmin
echo "Generating /pds/pds.env..."
printf "%s\n" \
"SERVICE_FQDN_PDS_3000=$${SERVICE_FQDN_PDS_3000}" \
"PDS_HOSTNAME=$${PDS_HOSTNAME}" \
"PDS_JWT_SECRET=$${PDS_JWT_SECRET}" \
"PDS_ADMIN_PASSWORD=$${PDS_ADMIN_PASSWORD}" \
"PDS_ADMIN_EMAIL=$${PDS_ADMIN_EMAIL}" \
"PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}" \
"PDS_DATA_DIRECTORY=$${PDS_DATA_DIRECTORY}" \
"PDS_BLOBSTORE_DISK_LOCATION=$${PDS_DATA_DIRECTORY}/blocks" \
"PDS_BLOB_UPLOAD_LIMIT=$${PDS_BLOB_UPLOAD_LIMIT}" \
"PDS_DID_PLC_URL=$${PDS_DID_PLC_URL}" \
"PDS_BSKY_APP_VIEW_URL=$${PDS_BSKY_APP_VIEW_URL}" \
"PDS_BSKY_APP_VIEW_DID=$${PDS_BSKY_APP_VIEW_DID}" \
"PDS_REPORT_SERVICE_URL=$${PDS_REPORT_SERVICE_URL}" \
"PDS_REPORT_SERVICE_DID=$${PDS_REPORT_SERVICE_DID}" \
"PDS_CRAWLERS=$${PDS_CRAWLERS}" \
"LOG_ENABLED=$${LOG_ENABLED}" \
> /pds/pds.env
echo "Launching PDS..."
exec node --enable-source-maps index.js
'
healthcheck:
test: ["CMD", "wget", "--spider", "http://127.0.0.1:3000/xrpc/_health"]
interval: 2s

View File

@@ -8,8 +8,8 @@ services:
bookstack:
image: lscr.io/linuxserver/bookstack:latest
environment:
- SERVICE_FQDN_BOOKSTACK_80
- APP_URL=${SERVICE_FQDN_BOOKSTACK}
- SERVICE_URL_BOOKSTACK_80
- APP_URL=${SERVICE_URL_BOOKSTACK}
- APP_KEY=${SERVICE_PASSWORD_APPKEY}
- PUID=1000
- PGID=1000

View File

@@ -8,7 +8,7 @@ services:
browserless:
image: ghcr.io/browserless/chromium
environment:
- SERVICE_FQDN_BROWSERLESS_3000
- SERVICE_URL_BROWSERLESS_3000
- TOKEN=$SERVICE_PASSWORD_BROWSERLESS
expose:
- 3000

View File

@@ -7,7 +7,7 @@ services:
budge:
image: lscr.io/linuxserver/budge:latest
environment:
- SERVICE_FQDN_BUDGE
- SERVICE_URL_BUDGE
- PUID=1000
- PGID=1000
- TZ=Europe/Madrid

View File

@@ -82,7 +82,7 @@ services:
proxy-service:
image: budibase/proxy
environment:
- SERVICE_FQDN_BUDIBASE_10000
- SERVICE_URL_BUDIBASE_10000
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
- PROXY_RATE_LIMIT_API_PER_SECOND=20
- APPS_UPSTREAM_URL=http://app-service:4002

View File

@@ -31,8 +31,8 @@ services:
environment:
- SECRET_KEY=$SERVICE_PASSWORD_64_BUGSINK
- CREATE_SUPERUSER=admin:$SERVICE_PASSWORD_BUGSINK
- SERVICE_FQDN_BUGSINK_8000
- BASE_URL=$SERVICE_FQDN_BUGSINK_8000
- SERVICE_URL_BUGSINK_8000
- BASE_URL=$SERVICE_URL_BUGSINK_8000
- DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}
depends_on:
mysql:

View File

@@ -12,14 +12,14 @@ services:
# Some variables still uses Calcom previous name, Calendso
#
# Full list https://github.com/calcom/cal.com/blob/main/.env.example
- SERVICE_FQDN_CALCOM_3000
- SERVICE_URL_CALCOM_3000
- NEXT_PUBLIC_LICENSE_CONSENT=agree
- NODE_ENV=production
- NEXT_PUBLIC_WEBAPP_URL=${SERVICE_FQDN_CALCOM}
- NEXT_PUBLIC_API_V2_URL=${SERVICE_FQDN_CALCOM}/api/v2
- NEXT_PUBLIC_WEBAPP_URL=${SERVICE_URL_CALCOM}
- NEXT_PUBLIC_API_V2_URL=${SERVICE_URL_CALCOM}/api/v2
# https://next-auth.js.org/configuration/options#nextauth_url
# From https://github.com/calcom/docker?tab=readme-ov-file#important-run-time-variables, it should be ${NEXT_PUBLIC_WEBAPP_URL}/api/auth
- NEXTAUTH_URL=${SERVICE_FQDN_CALCOM}/api/auth
- NEXTAUTH_URL=${SERVICE_URL_CALCOM}/api/auth
# It is highly recommended that the NEXTAUTH_SECRET must be overridden and very unique
# Use `openssl rand -base64 32` to generate a key
- NEXTAUTH_SECRET=${SERVICE_BASE64_CALCOMSECRET}
@@ -47,7 +47,7 @@ services:
- EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD}
- NEXT_PUBLIC_APP_NAME="Cal.com"
# More info on ALLOWED_HOSTNAMES https://github.com/calcom/cal.com/issues/12201
- ALLOWED_HOSTNAMES=["${SERVICE_FQDN_CALCOM}"]
- ALLOWED_HOSTNAMES=["${SERVICE_URL_CALCOM}"]
depends_on:
- postgresql
postgresql:

View File

@@ -8,7 +8,7 @@ services:
calibre-web:
image: lscr.io/linuxserver/calibre-web:latest
environment:
- SERVICE_FQDN_CALIBRE_8083
- SERVICE_URL_CALIBRE_8083
- PUID=1000
- PGID=1000
- TZ=${TZ:-Etc/UTC}

View File

@@ -10,12 +10,12 @@ services:
volumes:
- castopod-media:/var/www/castopod/public/media
environment:
- SERVICE_FQDN_CASTOPOD_8000
- SERVICE_URL_CASTOPOD_8000
- MYSQL_DATABASE=castopod
- MYSQL_USER=$SERVICE_USER_MYSQL
- MYSQL_PASSWORD=$SERVICE_PASSWORD_MYSQL
- CP_DISABLE_HTTPS=${CP_DISABLE_HTTPS:-1}
- CP_BASEURL=$SERVICE_FQDN_CASTOPOD
- CP_BASEURL=$SERVICE_URL_CASTOPOD
- CP_ANALYTICS_SALT=$SERVICE_REALBASE64_64_SALT
- CP_CACHE_HANDLER=redis
- CP_REDIS_HOST=redis

View File

@@ -10,8 +10,8 @@ services:
volumes:
- changedetection-data:/datastore
environment:
- SERVICE_FQDN_CHANGEDETECTION_5000
- BASE_URL=${SERVICE_FQDN_CHANGEDETECTION}
- SERVICE_URL_CHANGEDETECTION_5000
- BASE_URL=${SERVICE_URL_CHANGEDETECTION}
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
- PLAYWRIGHT_DRIVER_URL=${PLAYWRIGHT_DRIVER_URL:-ws://browser-sockpuppet-chrome:3000}

View File

@@ -8,13 +8,13 @@ services:
chaskiq:
image: chaskiq/chaskiq:latest
environment:
- SERVICE_FQDN_CHASKIQ_3000
- SERVICE_URL_CHASKIQ_3000
- REDIS_URL=redis://redis:6379/
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/${POSTGRES_DB:-chaskiq}
- POSTGRES_USER=$SERVICE_USER_POSTGRES
- SERVICE_URL=${SERVICE_URL_CHASKIQ}
- HOST=${SERVICE_FQDN_CHASKIQ_3000}
- ASSET_HOST=${SERVICE_FQDN_CHASKIQ_3000}
- HOST=${SERVICE_URL_CHASKIQ_3000}
- ASSET_HOST=${SERVICE_URL_CHASKIQ_3000}
- WS=wss://${SERVICE_URL_CHASKIQ}/cable
- SNS_CONFIGURATION_SET=metrics
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
@@ -80,8 +80,8 @@ services:
- REDIS_URL=redis://redis:6379/
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/${POSTGRES_DB:-chaskiq}
- POSTGRES_USER=$SERVICE_USER_POSTGRES
- HOST=${SERVICE_FQDN_CHASKIQ_3000}
- ASSET_HOST=${SERVICE_FQDN_CHASKIQ_3000}
- HOST=${SERVICE_URL_CHASKIQ_3000}
- ASSET_HOST=${SERVICE_URL_CHASKIQ_3000}
- WS=wss://${SERVICE_URL_CHASKIQ}/cable
- SNS_CONFIGURATION_SET=metrics
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}

View File

@@ -11,9 +11,9 @@ services:
- postgres
- redis
environment:
- SERVICE_FQDN_CHATWOOT_3000
- SERVICE_URL_CHATWOOT_3000
- SECRET_KEY_BASE=$SERVICE_PASSWORD_CHATWOOT
- FRONTEND_URL=${SERVICE_FQDN_CHATWOOT}
- FRONTEND_URL=${SERVICE_URL_CHATWOOT}
- DEFAULT_LOCALE=${CHATWOOT_DEFAULT_LOCALE}
- FORCE_SSL=${FORCE_SSL:-false}
- ENABLE_ACCOUNT_SIGNUP=${ENABLE_ACCOUNT_SIGNUP:-false}
@@ -54,7 +54,7 @@ services:
- redis
environment:
- SECRET_KEY_BASE=$SERVICE_PASSWORD_CHATWOOT
- FRONTEND_URL=${SERVICE_FQDN_CHATWOOT}
- FRONTEND_URL=${SERVICE_URL_CHATWOOT}
- DEFAULT_LOCALE=${CHATWOOT_DEFAULT_LOCALE}
- FORCE_SSL=${FORCE_SSL:-false}
- ENABLE_ACCOUNT_SIGNUP=${ENABLE_ACCOUNT_SIGNUP:-false}

View File

@@ -8,14 +8,14 @@ services:
client:
image: bluewaveuptime/uptime_client:latest
environment:
- SERVICE_FQDN_CHECKMATE_80
- UPTIME_APP_API_BASE_URL=${SERVICE_FQDN_CHECKMATESERVER_5000}/api/v1
- SERVICE_URL_CHECKMATE_80
- UPTIME_APP_API_BASE_URL=${SERVICE_URL_CHECKMATESERVER_5000}/api/v1
depends_on:
- server
server:
image: bluewaveuptime/uptime_server:latest
environment:
- SERVICE_FQDN_CHECKMATESERVER_5000
- SERVICE_URL_CHECKMATESERVER_5000
- JWT_SECRET=${SERVICE_PASSWORD_64_JWT}
- REFRESH_TOKEN_SECRET=${SERVICE_PASSWORD_64_REFRESH}
- SYSTEM_EMAIL_ADDRESS=${SYSTEM_EMAIL_ADDRESS:-test@example.com}

View File

@@ -10,7 +10,7 @@ services:
volumes:
- chroma-data:/data
environment:
- SERVICE_FQDN_CHROMA_8000
- SERVICE_URL_CHROMA_8000
- IS_PERSISTENT=TRUE
- PERSIST_DIRECTORY=/data
healthcheck:

View File

@@ -9,7 +9,7 @@ services:
volumes:
- classicpress-files:/var/www/html
environment:
- SERVICE_FQDN_CLASSICPRESS
- SERVICE_URL_CLASSICPRESS
- CLASSICPRESS_DB_HOST=mariadb
- CLASSICPRESS_DB_USER=$SERVICE_USER_CLASSICPRESS
- CLASSICPRESS_DB_PASSWORD=$SERVICE_PASSWORD_CLASSICPRESS

View File

@@ -9,7 +9,7 @@ services:
volumes:
- classicpress-files:/var/www/html
environment:
- SERVICE_FQDN_CLASSICPRESS
- SERVICE_URL_CLASSICPRESS
- CLASSICPRESS_DB_HOST=mysql
- CLASSICPRESS_DB_USER=$SERVICE_USER_CLASSICPRESS
- CLASSICPRESS_DB_PASSWORD=$SERVICE_PASSWORD_CLASSICPRESS

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