Merge pull request #5496 from coollabsio/next

v4.0.0-beta.404
This commit is contained in:
Andras Bacsai
2025-04-03 09:42:55 +02:00
committed by GitHub
25 changed files with 503 additions and 402 deletions

View File

@@ -238,7 +238,7 @@ After completing these steps, you'll have a fresh development setup.
### Contributing a New Service
To add a new service to Coolify, please refer to our documentation:
[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service)
[Adding a New Service](https://coolify.io/docs/get-started/contribute/service)
### Contributing to Documentation

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction;
class StopProxy
{
use AsAction;
public function handle(Server $server, bool $forceStop = true)
{
try {
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$timeout = 30;
$process = $this->stopContainer($containerName, $timeout);
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName, $server);
break;
}
usleep(100000);
}
$this->removeContainer($containerName, $server);
} catch (\Throwable $e) {
return handleError($e);
} finally {
$server->proxy->force_stop = $forceStop;
$server->proxy->status = 'exited';
$server->save();
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function forceStopContainer(string $containerName, Server $server)
{
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
}
private function removeContainer(string $containerName, Server $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
}

View File

@@ -2881,198 +2881,198 @@ class ApplicationsController extends Controller
);
}
#[OA\Post(
summary: 'Execute Command',
description: "Execute a command on the application's current container.",
path: '/applications/{uuid}/execute',
operationId: 'execute-command-application',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Command to execute.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
],
),
),
),
responses: [
new OA\Response(
response: 200,
description: "Execute a command on the application's current container.",
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Command executed.'],
'response' => ['type' => 'string'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function execute_command_by_uuid(Request $request)
{
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
$allowedFields = ['command'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'command' => 'string|required',
]);
// #[OA\Post(
// summary: 'Execute Command',
// description: "Execute a command on the application's current container.",
// path: '/applications/{uuid}/execute',
// operationId: 'execute-command-application',
// security: [
// ['bearerAuth' => []],
// ],
// tags: ['Applications'],
// parameters: [
// new OA\Parameter(
// name: 'uuid',
// in: 'path',
// description: 'UUID of the application.',
// required: true,
// schema: new OA\Schema(
// type: 'string',
// format: 'uuid',
// )
// ),
// ],
// requestBody: new OA\RequestBody(
// required: true,
// description: 'Command to execute.',
// content: new OA\MediaType(
// mediaType: 'application/json',
// schema: new OA\Schema(
// type: 'object',
// properties: [
// 'command' => ['type' => 'string', 'description' => 'Command to execute.'],
// ],
// ),
// ),
// ),
// responses: [
// new OA\Response(
// response: 200,
// description: "Execute a command on the application's current container.",
// content: [
// new OA\MediaType(
// mediaType: 'application/json',
// schema: new OA\Schema(
// type: 'object',
// properties: [
// 'message' => ['type' => 'string', 'example' => 'Command executed.'],
// 'response' => ['type' => 'string'],
// ]
// )
// ),
// ]
// ),
// new OA\Response(
// response: 401,
// ref: '#/components/responses/401',
// ),
// new OA\Response(
// response: 400,
// ref: '#/components/responses/400',
// ),
// new OA\Response(
// response: 404,
// ref: '#/components/responses/404',
// ),
// ]
// )]
// public function execute_command_by_uuid(Request $request)
// {
// // TODO: Need to review this from security perspective, to not allow arbitrary command execution
// $allowedFields = ['command'];
// $teamId = getTeamIdFromToken();
// if (is_null($teamId)) {
// return invalidTokenResponse();
// }
// $uuid = $request->route('uuid');
// if (! $uuid) {
// return response()->json(['message' => 'UUID is required.'], 400);
// }
// $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
// if (! $application) {
// return response()->json(['message' => 'Application not found.'], 404);
// }
// $return = validateIncomingRequest($request);
// if ($return instanceof \Illuminate\Http\JsonResponse) {
// return $return;
// }
// $validator = customApiValidator($request->all(), [
// 'command' => 'string|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.');
}
}
// $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);
}
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => $errors,
// ], 422);
// }
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
$status = getContainerStatus($application->destination->server, $container['Names']);
// $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
// $status = getContainerStatus($application->destination->server, $container['Names']);
if ($status !== 'running') {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
// if ($status !== 'running') {
// return response()->json([
// 'message' => 'Application is not running.',
// ], 400);
// }
$commands = collect([
executeInDocker($container['Names'], $request->command),
]);
// $commands = collect([
// executeInDocker($container['Names'], $request->command),
// ]);
$res = instant_remote_process(command: $commands, server: $application->destination->server);
// $res = instant_remote_process(command: $commands, server: $application->destination->server);
return response()->json([
'message' => 'Command executed.',
'response' => $res,
]);
}
// return response()->json([
// 'message' => 'Command executed.',
// 'response' => $res,
// ]);
// }
private function validateDataApplications(Request $request, Server $server)
{
$teamId = getTeamIdFromToken();
// private function validateDataApplications(Request $request, Server $server)
// {
// $teamId = getTeamIdFromToken();
// Validate ports_mappings
if ($request->has('ports_mappings')) {
$ports = [];
foreach (explode(',', $request->ports_mappings) as $portMapping) {
$port = explode(':', $portMapping);
if (in_array($port[0], $ports)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'ports_mappings' => 'The first number before : should be unique between mappings.',
],
], 422);
}
$ports[] = $port[0];
}
}
// Validate custom_labels
if ($request->has('custom_labels')) {
if (! isBase64Encoded($request->custom_labels)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_labels' => 'The custom_labels should be base64 encoded.',
],
], 422);
}
$customLabels = base64_decode($request->custom_labels);
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_labels' => 'The custom_labels should be base64 encoded.',
],
], 422);
}
}
if ($request->has('domains') && $server->isProxyShouldRun()) {
$uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$errors = [];
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
$errors[] = 'Invalid domain: '.$domain;
}
// // Validate ports_mappings
// if ($request->has('ports_mappings')) {
// $ports = [];
// foreach (explode(',', $request->ports_mappings) as $portMapping) {
// $port = explode(':', $portMapping);
// if (in_array($port[0], $ports)) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'ports_mappings' => 'The first number before : should be unique between mappings.',
// ],
// ], 422);
// }
// $ports[] = $port[0];
// }
// }
// // Validate custom_labels
// if ($request->has('custom_labels')) {
// if (! isBase64Encoded($request->custom_labels)) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ],
// ], 422);
// }
// $customLabels = base64_decode($request->custom_labels);
// if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ],
// ], 422);
// }
// }
// if ($request->has('domains') && $server->isProxyShouldRun()) {
// $uuid = $request->uuid;
// $fqdn = $request->domains;
// $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
// $fqdn = str($fqdn)->replaceStart(',', '')->trim();
// $errors = [];
// $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
// if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
// $errors[] = 'Invalid domain: '.$domain;
// }
return str($domain)->trim()->lower();
});
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'One of the domain is already used.',
],
], 422);
}
}
}
// return str($domain)->trim()->lower();
// });
// if (count($errors) > 0) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => $errors,
// ], 422);
// }
// if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'domains' => 'One of the domain is already used.',
// ],
// ], 422);
// }
// }
// }
}

View File

@@ -329,13 +329,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$this->write_deployment_configurations();
}
$this->execute_remote_command(
[
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
'hidden' => true,
'ignore_errors' => true,
]
);
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
@@ -1366,13 +1361,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->execute_remote_command(
[
'command' => "docker rm -f {$this->deployment_uuid}",
'ignore_errors' => true,
'hidden' => true,
]
);
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
$this->execute_remote_command(
[
$runCommand,

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Jobs;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function __construct(public Server $server) {}
public function handle()
{
try {
StopProxy::run($this->server);
$this->server->proxy->force_stop = false;
$this->server->save();
StartProxy::run($this->server, force: true);
CheckProxy::run($this->server, true);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View File

@@ -4,11 +4,10 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Events\ProxyStatusChanged;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
@@ -65,6 +64,7 @@ class Deploy extends Component
public function restart()
{
try {
RestartProxyJob::dispatch($this->server);
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -97,43 +97,10 @@ class Deploy extends Component
public function stop(bool $forceStop = true)
{
try {
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$timeout = 30;
$process = $this->stopContainer($containerName, $timeout);
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;
}
usleep(100000);
}
$this->removeContainer($containerName);
StopProxy::run($this->server, $forceStop);
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->server->proxy->force_stop = $forceStop;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->dispatch('proxyStatusUpdated');
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function forceStopContainer(string $containerName)
{
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
}
private function removeContainer(string $containerName)
{
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
}
}

View File

@@ -15,6 +15,8 @@ class SettingsBackup extends Component
{
public InstanceSettings $settings;
public Server $server;
public ?StandalonePostgresql $database = null;
public ScheduledDatabaseBackup|null|array $backup = [];
@@ -46,6 +48,7 @@ class SettingsBackup extends Component
return redirect()->route('dashboard');
} else {
$settings = instanceSettings();
$this->server = Server::findOrFail(0);
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
@@ -60,6 +63,10 @@ class SettingsBackup extends Component
$this->database->save();
}
$this->backup = $this->database->scheduledBackups->first();
if ($this->backup && ! $this->server->isFunctional()) {
$this->backup->enabled = false;
$this->backup->save();
}
$this->executions = $this->backup->executions;
}
$this->settings = $settings;

View File

@@ -4,7 +4,7 @@ namespace App\Livewire;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Notifications\Test;
use App\Notifications\TransactionalEmails\Test;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;

View File

@@ -12,8 +12,9 @@ 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) {
if ($useInstanceEmailSettings || $isTransactionalEmail) {
$settings = instanceSettings();
} else {
$settings = $notifiable->emailNotificationSettings;
@@ -49,8 +50,8 @@ class EmailChannel
$settings->smtp_port,
$encryption
);
$transport->setUsername($settings->smtp_username);
$transport->setPassword($settings->smtp_password);
$transport->setUsername($settings->smtp_username ?? '');
$transport->setPassword($settings->smtp_password ?? '');
$mailer = new \Symfony\Component\Mailer\Mailer($transport);

View File

@@ -16,9 +16,10 @@ class InvitationLink extends CustomEmailNotification
return [TransactionalEmailChannel::class];
}
public function __construct(public User $user)
public function __construct(public User $user, public bool $isTransactionalEmail)
{
$this->onQueue('high');
$this->isTransactionalEmail = true;
}
public function toMail(): MailMessage

View File

@@ -17,10 +17,11 @@ class ResetPassword extends Notification
public InstanceSettings $settings;
public function __construct($token)
public function __construct($token, public bool $isTransactionalEmail)
{
$this->settings = instanceSettings();
$this->token = $token;
$this->isTransactionalEmail = true;
}
public static function createUrlUsing($callback)

View File

@@ -8,9 +8,10 @@ use Illuminate\Notifications\Messages\MailMessage;
class Test extends CustomEmailNotification
{
public function __construct(public string $emails)
public function __construct(public string $emails, public string $isTransactionalEmail)
{
$this->onQueue('high');
$this->isTransactionalEmail = true;
}
public function via(): array

40
lang/az.json Normal file
View File

@@ -0,0 +1,40 @@
{
"auth.login": "Daxil ol",
"auth.login.authentik": "Authentik ilə daxil ol",
"auth.login.azure": "Azure ilə daxil ol",
"auth.login.bitbucket": "Bitbucket ilə daxil ol",
"auth.login.github": "Github ilə daxil ol",
"auth.login.gitlab": "GitLab ilə daxil ol",
"auth.login.google": "Google ilə daxil ol",
"auth.login.infomaniak": "Infomaniak ilə daxil ol",
"auth.already_registered": "Qeytiyatınız var?",
"auth.confirm_password": "Şifrəni təsdiqləyin",
"auth.forgot_password": "Şifrəmi unutdum",
"auth.forgot_password_send_email": "Şifrəni sıfırlamaq üçün e-poçt göndər",
"auth.register_now": "Qeydiyyat",
"auth.logout": ıxış",
"auth.register": "Qeydiyyat",
"auth.registration_disabled": "Qeydiyyat bağlıdır. Administratorla əlaqə saxlayın.",
"auth.reset_password": "Şifrənin bərpası",
"auth.failed": "Bu məlumatlar bizim qeydlərimizlə uyğun gəlmir.",
"auth.failed.callback": "Giriş təminatçısından geri çağırma işlənə bilmədi.",
"auth.failed.password": "Daxil etdiyiniz şifrə yanlışdır.",
"auth.failed.email": "Bu e-poçt ünvanı ilə istifadəçi tapılmadı.",
"auth.throttle": "Çox sayda uğursuz giriş cəhdi. Zəhmət olmasa :seconds saniyə sonra yenidən cəhd edin.",
"input.name": "Ad",
"input.email": "E-poçt",
"input.password": "Şifrə",
"input.password.again": "Şifrəni təkrar daxil edin",
"input.code": "Bir dəfəlik kod",
"input.recovery_code": "Bərpa kodu",
"button.save": "Yadda saxla",
"repository.url": "<span class='text-helper'>Nümunələr</span><br>Publik repozitoriyalar üçün <span class='text-helper'>https://...</span> istifadə edin.<br>Özəl repozitoriyalar üçün <span class='text-helper'>git@...</span> istifadə edin.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch-ı seçiləcək<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch-ı seçiləcək.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch-ı seçiləcək.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch-ı seçiləcək.",
"service.stop": "Bu xidmət dayandırılacaq.",
"resource.docker_cleanup": "Docker təmizlənməsini işə salın (istifadə olunmayan şəkillər və builder keşini silin).",
"resource.non_persistent": "Bütün qeyri-daimi məlumatlar silinəcək.",
"resource.delete_volumes": "Bu resursla əlaqəli bütün həcm məlumatları tamamilə silinəcək.",
"resource.delete_connected_networks": "Bu resursla əlaqəli bütün əvvəlcədən təyin olunmamış şəbəkələr tamamilə silinəcək.",
"resource.delete_configurations": "Serverdən bütün konfiqurasiya faylları tamamilə silinəcək.",
"database.delete_backups_locally": "Bütün ehtiyat nüsxələr lokal yaddaşdan tamamilə silinəcək.",
"warning.sslipdomain": "Konfiqurasiya yadda saxlanıldı, lakin sslip domeni ilə https <span class='dark:text-red-500 text-red-500 font-bold'>TÖVSİYƏ EDİLMİR</span>, çünki Let's Encrypt serverləri bu ümumi domenlə məhdudlaşdırılır (SSL sertifikatının təsdiqlənməsi uğursuz olacaq). <br><br>Əvəzində öz domeninizdən istifadə edin."
}

40
lang/id.json Normal file
View File

@@ -0,0 +1,40 @@
{
"auth.login": "Masuk",
"auth.login.authentik": "Masuk dengan Authentik",
"auth.login.azure": "Masuk dengan Microsoft",
"auth.login.bitbucket": "Masuk dengan Bitbucket",
"auth.login.github": "Masuk dengan GitHub",
"auth.login.gitlab": "Masuk dengan Gitlab",
"auth.login.google": "Masuk dengan Google",
"auth.login.infomaniak": "Masuk dengan Infomaniak",
"auth.already_registered": "Sudah terdaftar?",
"auth.confirm_password": "Konfirmasi kata sandi",
"auth.forgot_password": "Lupa kata sandi",
"auth.forgot_password_send_email": "Kirim email reset kata sandi",
"auth.register_now": "Daftar",
"auth.logout": "Keluar",
"auth.register": "Daftar",
"auth.registration_disabled": "Pendaftaran dinonaktifkan. Harap hubungi administrator.",
"auth.reset_password": "Reset kata sandi",
"auth.failed": "Kredensial ini tidak cocok dengan catatan kami.",
"auth.failed.callback": "Gagal memproses callback dari penyedia login.",
"auth.failed.password": "Kata sandi yang diberikan salah.",
"auth.failed.email": "Kami tidak dapat menemukan pengguna dengan alamat e-mail tersebut.",
"auth.throttle": "Terlalu banyak percobaan login. Silakan coba lagi dalam :seconds detik.",
"input.name": "Nama",
"input.email": "Email",
"input.password": "Kata sandi",
"input.password.again": "Kata sandi lagi",
"input.code": "Kode sekali pakai",
"input.recovery_code": "Kode pemulihan",
"button.save": "Simpan",
"repository.url": "<span class='text-helper'>Contoh</span><br>Untuk repositori Publik, gunakan <span class='text-helper'>https://...</span>.<br>Untuk repositori Privat, gunakan <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples cabang <span class='text-helper'>main</span> akan dipilih<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify cabang <span class='text-helper'>nodejs-fastify</span> akan dipilih.<br>https://gitea.com/sedlav/expressjs.git cabang <span class='text-helper'>main</span> akan dipilih.<br>https://gitlab.com/andrasbacsai/nodejs-example.git cabang <span class='text-helper'>main</span> akan dipilih.",
"service.stop": "Layanan ini akan dihentikan.",
"resource.docker_cleanup": "Jalankan Pembersihan Docker (hapus gambar yang tidak digunakan dan cache builder).",
"resource.non_persistent": "Semua data non-persisten akan dihapus.",
"resource.delete_volumes": "Hapus permanen semua volume yang terkait dengan sumber daya ini.",
"resource.delete_connected_networks": "Hapus permanen semua jaringan non-predefined yang terkait dengan sumber daya ini.",
"resource.delete_configurations": "Hapus permanen semua file konfigurasi dari server.",
"database.delete_backups_locally": "Semua backup akan dihapus permanen dari penyimpanan lokal.",
"warning.sslipdomain": "Konfigurasi Anda disimpan, tetapi domain sslip dengan https <span class='font-bold text-red-500 dark:text-red-500'>TIDAK</span> direkomendasikan, karena server Let's Encrypt dengan domain publik ini dibatasi (validasi sertifikat SSL akan gagal). <br><br>Gunakan domain Anda sendiri sebagai gantinya."
}

40
lang/pt-br.json Normal file
View File

@@ -0,0 +1,40 @@
{
"auth.login": "Entrar",
"auth.login.authentik": "Entrar com Authentik",
"auth.login.azure": "Entrar com Microsoft",
"auth.login.bitbucket": "Entrar com Bitbucket",
"auth.login.github": "Entrar com GitHub",
"auth.login.gitlab": "Entrar com Gitlab",
"auth.login.google": "Entrar com Google",
"auth.login.infomaniak": "Entrar com Infomaniak",
"auth.already_registered": "Já tem uma conta?",
"auth.confirm_password": "Confirmar senha",
"auth.forgot_password": "Esqueceu a senha",
"auth.forgot_password_send_email": "Enviar e-mail para redefinir senha",
"auth.register_now": "Cadastre-se",
"auth.logout": "Sair",
"auth.register": "Cadastrar",
"auth.registration_disabled": "O registro está desativado. Por favor, contate o administrador.",
"auth.reset_password": "Redefinir senha",
"auth.failed": "Essas credenciais não correspondem aos nossos registros.",
"auth.failed.callback": "Falha ao processar o callback do provedor de login.",
"auth.failed.password": "A senha fornecida está incorreta.",
"auth.failed.email": "Não encontramos nenhum usuário com esse endereço de e-mail.",
"auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.",
"input.name": "Nome",
"input.email": "E-mail",
"input.password": "Senha",
"input.password.again": "Senha novamente",
"input.code": "Código de uso único",
"input.recovery_code": "Código de recuperação",
"button.save": "Salvar",
"repository.url": "<span class='text-helper'>Exemplos</span><br>Para repositórios públicos, use <span class='text-helper'>https://...</span>.<br>Para repositórios privados, use <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch será selecionado<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch será selecionado.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch será selecionado.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch será selecionado.",
"service.stop": "Este serviço será parado.",
"resource.docker_cleanup": "Executar limpeza do Docker (remover imagens não utilizadas e cache de build).",
"resource.non_persistent": "Todos os dados não persistentes serão excluídos.",
"resource.delete_volumes": "Excluir permanentemente todos os volumes associados a este recurso.",
"resource.delete_connected_networks": "Excluir permanentemente todas as redes não predefinidas associadas a este recurso.",
"resource.delete_configurations": "Excluir permanentemente todos os arquivos de configuração do servidor.",
"database.delete_backups_locally": "Todos os backups serão excluídos permanentemente do armazenamento local.",
"warning.sslipdomain": "Sua configuração foi salva, mas o domínio sslip com https <span class='dark:text-red-500 text-red-500 font-bold'>NÃO</span> é recomendado, porque os servidores do Let's Encrypt com este domínio público têm limitação de taxa (a validação do certificado SSL falhará). <br><br>Use seu próprio domínio em vez disso."
}

View File

@@ -27,5 +27,13 @@
"input.code": "Tek Kullanımlık Kod",
"input.recovery_code": "Kurtarma Kodu",
"button.save": "Kaydet",
"repository.url": "<span class='text-helper'>Örnekler</span><br>Halka açık depolar için <span class='text-helper'>https://...</span> kullanın.<br>Özel depolar için <span class='text-helper'>git@...</span> kullanın.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> dalı seçilecek<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> dalı seçilecek.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> dalı seçilecek.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> dalı seçilecek."
"repository.url": "<span class='text-helper'>Örnekler</span><br>Halka açık depolar için <span class='text-helper'>https://...</span> kullanın.<br>Özel depolar için <span class='text-helper'>git@...</span> kullanın.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> dalı seçilecek<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> dalı seçilecek.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> dalı seçilecek.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> dalı seçilecek.",
"service.stop": "Bu servis durdurulacak.",
"resource.docker_cleanup": "Docker temizliği çalıştır (kullanılmayan imajları ve oluşturucu önbelleğini kaldır).",
"resource.non_persistent": "Tüm kalıcı olmayan veriler silinecek.",
"resource.delete_volumes": "Bu kaynakla ilişkili tüm hacimler kalıcı olarak silinecek.",
"resource.delete_connected_networks": "Bu kaynakla ilişkili önceden tanımlanmamış tüm ağlar kalıcı olarak silinecek.",
"resource.delete_configurations": "Sunucudaki tüm yapılandırma dosyaları kalıcı olarak silinecek.",
"database.delete_backups_locally": "Tüm yedekler yerel depolamadan kalıcı olarak silinecek.",
"warning.sslipdomain": "Yapılandırmanız kaydedildi, ancak sslip domain ile https <span class='dark:text-red-500 text-red-500 font-bold'>ÖNERİLMEZ</span>, çünkü Let's Encrypt sunucuları bu genel domain ile sınırlandırılmıştır (SSL sertifikası doğrulaması başarısız olur). <br><br>Bunun yerine kendi domaininizi kullanın."
}

View File

@@ -2771,80 +2771,6 @@
]
}
},
"\/applications\/{uuid}\/execute": {
"post": {
"tags": [
"Applications"
],
"summary": "Execute Command",
"description": "Execute a command on the application's current container.",
"operationId": "execute-command-application",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"description": "Command to execute.",
"required": true,
"content": {
"application\/json": {
"schema": {
"properties": {
"command": {
"type": "string",
"description": "Command to execute."
}
},
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Execute a command on the application's current container.",
"content": {
"application\/json": {
"schema": {
"properties": {
"message": {
"type": "string",
"example": "Command executed."
},
"response": {
"type": "string"
}
},
"type": "object"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/databases": {
"get": {
"tags": [

View File

@@ -1905,52 +1905,6 @@ paths:
security:
-
bearerAuth: []
'/applications/{uuid}/execute':
post:
tags:
- Applications
summary: 'Execute Command'
description: "Execute a command on the application's current container."
operationId: execute-command-application
parameters:
-
name: uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
requestBody:
description: 'Command to execute.'
required: true
content:
application/json:
schema:
properties:
command:
type: string
description: 'Command to execute.'
type: object
responses:
'200':
description: "Execute a command on the application's current container."
content:
application/json:
schema:
properties:
message: { type: string, example: 'Command executed.' }
response: { type: string }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
/databases:
get:
tags:

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.403"
"version": "4.0.0-beta.404"
},
"nightly": {
"version": "4.0.0-beta.404"
"version": "4.0.0-beta.405"
},
"helper": {
"version": "1.0.8"

View File

@@ -1,8 +1,9 @@
@props(['text'])
<div class="relative" x-data="{ copied: false }">
<div class="relative" x-data="{ copied: false, isSecure: window.isSecureContext }">
<input type="text" value="{{ $text }}" readonly class="input">
<button
x-show="isSecure"
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from($text) }}); setTimeout(() => copied = false, 1000)"
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
title="Copy to clipboard">

View File

@@ -6,7 +6,7 @@
<div class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Backup</h2>
@if (isset($database))
@if (isset($database) && $server->isFunctional())
<x-forms.button type="submit" wire:click="submit">
Save
</x-forms.button>
@@ -14,26 +14,39 @@
</div>
<div class="pb-4">Backup configuration for Coolify instance.</div>
<div>
@if (isset($database) && isset($backup))
<div class="flex flex-col gap-3 pb-4">
<div class="flex gap-2">
<x-forms.input label="UUID" readonly id="uuid" />
<x-forms.input label="Name" readonly id="name" />
<x-forms.input label="Description" id="description" />
@if ($server->isFunctional())
@if (isset($database) && isset($backup))
<div class="flex flex-col gap-3 pb-4">
<div class="flex gap-2">
<x-forms.input label="UUID" readonly id="uuid" />
<x-forms.input label="Name" readonly id="name" />
<x-forms.input label="Description" id="description" />
</div>
<div class="flex gap-2">
<x-forms.input label="User" readonly id="postgres_user" />
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
</div>
</div>
<div class="flex gap-2">
<x-forms.input label="User" readonly id="postgres_user" />
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<div class="py-4">
<livewire:project.database.backup-executions :backup="$backup" />
</div>
</div>
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<div class="py-4">
<livewire:project.database.backup-executions :backup="$backup" />
</div>
@else
To configure automatic backup for your Coolify instance, you first need to add a database resource
into Coolify.
<x-forms.button class="mt-2" wire:click="addCoolifyDatabase">Configure Backup</x-forms.button>
@endif
@else
To configure automatic backup for your Coolify instance, you first need to add a database resource
into Coolify.
<x-forms.button class="mt-2" wire:click="addCoolifyDatabase">Configure Backup</x-forms.button>
<div class="p-6 bg-red-500/10 rounded-lg border border-red-500/20">
<div class="text-red-500 font-medium mb-4">
Instance Backup is currently disabled because the localhost server is not properly validated.
Please validate your server to enable Instance Backup.
</div>
<a href="{{ route('server.show', [$server->uuid]) }}"
class="text-black hover:text-gray-700 dark:text-white dark:hover:text-gray-200 underline">
Go to Server Settings to Validate
</a>
</div>
@endif
</div>
</div>

View File

@@ -39,9 +39,15 @@ if ! docker network inspect coolify >/dev/null 2>&1; then
fi
# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
# Check if Docker config file exists
DOCKER_CONFIG_MOUNT=""
if [ -f /root/.docker/config.json ]; then
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
fi
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
echo "docker-compose.custom.yml detected." >>$LOGFILE
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
else
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1
fi

View File

@@ -3,17 +3,18 @@
# tags: python, error-tracking, django, mysql
# logo: svgs/bugsink.svg
# port: 8000
services:
mysql:
image: 'mysql:latest'
restart: unless-stopped
environment:
- 'MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT}'
- 'MYSQL_DATABASE=${MYSQL_DATABASE:-bugsink}'
- 'MYSQL_USER=${SERVICE_USER_BUGSINK}'
- 'MYSQL_PASSWORD=${SERVICE_PASSWORD_BUGSINK}'
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT}
- MYSQL_DATABASE=${MYSQL_DATABASE:-bugsink}
- MYSQL_USER=${SERVICE_USER_BUGSINK}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_BUGSINK}
volumes:
- 'my-datavolume:/var/lib/mysql'
- my-datavolume:/var/lib/mysql
healthcheck:
test:
- CMD
@@ -29,13 +30,15 @@ services:
restart: unless-stopped
environment:
- SECRET_KEY=$SERVICE_PASSWORD_64_BUGSINK
- 'CREATE_SUPERUSER=admin:${SERVICE_PASSWORD_BUGSINK}'
- CREATE_SUPERUSER=admin:$SERVICE_PASSWORD_BUGSINK
- SERVICE_FQDN_BUGSINK_8000
- 'BASE_URL=${SERVICE_FQDN_BUGSINK_8000}'
- 'DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}'
- BASE_URL=$SERVICE_FQDN_BUGSINK_8000
- DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}
depends_on:
mysql:
condition: service_healthy
volumes:
my-datavolume:
healthcheck:
test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8000/\").raise_for_status()'"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -271,7 +271,7 @@
"bugsink": {
"documentation": "https://www.bugsink.com/docs/?utm_source=coolify.io",
"slogan": "Self-hosted Error Tracking",
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiR7U0VSVklDRV9QQVNTV09SRF9CVUdTSU5LfScKICAgICAgLSBTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0JBU0VfVVJMPSR7U0VSVklDRV9GUUROX0JVR1NJTktfODAwMH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CnZvbHVtZXM6CiAgbXktZGF0YXZvbHVtZTogbnVsbAo=",
"compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTksnCiAgICAgIC0gU0VSVklDRV9GUUROX0JVR1NJTktfODAwMAogICAgICAtIEJBU0VfVVJMPSRTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3B5dGhvbiAtYyAnJ2ltcG9ydCByZXF1ZXN0czsgcmVxdWVzdHMuZ2V0KCJodHRwOi8vbG9jYWxob3N0OjgwMDAvIikucmFpc2VfZm9yX3N0YXR1cygpJycnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"tags": [
"python",
"error-tracking",

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.403"
"version": "4.0.0-beta.404"
},
"nightly": {
"version": "4.0.0-beta.404"
"version": "4.0.0-beta.405"
},
"helper": {
"version": "1.0.8"